@agentorg/cli 0.0.7 → 0.0.9
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 +3 -3
- package/dist/node_modules/@agentorg/computer/dist/types/generated/schema.d.ts +3 -0
- package/dist/node_modules/@agentorg/computer/package.json +1 -1
- package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.js +3 -3
- package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.d.ts +1 -1
- package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.js +34 -30
- package/dist/node_modules/@agentorg/computer-connector/package.json +1 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/index.d.ts +0 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/index.js +0 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/manager.d.ts +1 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/manager.js +1 -1
- package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +40 -17
- package/dist/node_modules/@agentorg/looper/dist/src/loop.js +142 -69
- package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +62 -18
- package/dist/node_modules/@agentorg/looper/dist/src/looper.js +588 -108
- package/dist/node_modules/@agentorg/looper/dist/src/main.d.ts +90 -0
- package/dist/node_modules/@agentorg/looper/dist/src/main.js +205 -0
- package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +156 -16
- package/dist/node_modules/@agentorg/looper/dist/src/record.js +292 -58
- 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 +290 -375
- package/dist/node_modules/@agentorg/shell/dist/app.js +141 -144
- package/dist/node_modules/assistant/dist/src/chats.d.ts +14 -7
- package/dist/node_modules/assistant/dist/src/chats.js +24 -25
- package/dist/node_modules/assistant/dist/src/engine.js +11 -5
- package/dist/node_modules/chatgpt/dist/src/account.js +1 -1
- package/dist/src/gateway.js +0 -3
- package/package.json +1 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/folders.d.ts +0 -19
- package/dist/node_modules/@agentorg/interfaces/dist/src/folders.js +0 -25
- package/dist/src/folders.d.ts +0 -2
- package/dist/src/folders.js +0 -45
|
@@ -1,30 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The looper: the loops of whoever hosts it
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The looper: the loops of whoever hosts it. Messages come in on threads, which are names its host
|
|
3
|
+
* gives; a thread's messages are kept under `thread/<name>/` in the store. What answers them is a
|
|
4
|
+
* piece of work, with a loop of its own, or, when a message needs no work, the main loop.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* thread
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Intake is code. A message in a thread that a piece of work follows is given to that work. One in
|
|
7
|
+
* a thread nothing follows is kept, and the main loop decides on the thread: it answers itself, and
|
|
8
|
+
* the thread stays no work's; or it says whose the message is, new work, which has the thread's
|
|
9
|
+
* name as its id, or work there is, in a turn or at rest, which then follows that thread too. The
|
|
10
|
+
* main loop makes one decision at a time, and one for a thread on all that waits in it. A piece
|
|
11
|
+
* of work answers in the thread of the newest message it has read.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* turn
|
|
15
|
-
*
|
|
13
|
+
* One piece of work is stopped by saying so. The main loop may answer a message in a thread
|
|
14
|
+
* nothing follows by stopping the turn of work it was told of; and it is told of each message that
|
|
15
|
+
* joins a turn of the work its thread follows, beside that work getting it, to stop that turn if
|
|
16
|
+
* the message asks. The turn is cut and the work is at rest. What the main loop says of it is
|
|
17
|
+
* said where the message was: in the work's own record when it joined the turn, for the work to
|
|
18
|
+
* read next, and otherwise in the asking thread alone, which stays no work's.
|
|
19
|
+
*
|
|
20
|
+
* Turns run side by side, each in a place; the main loop takes none. There are `PLACES` of them,
|
|
21
|
+
* and one more that only work already begun (it has done something) may take, so new work never
|
|
22
|
+
* keeps a message from work that is under way. A turn that finds no place waits for one, in the
|
|
23
|
+
* order it came. While begun work keeps five turns running, new work goes on waiting. `WAITING`
|
|
24
|
+
* turns may wait; past that a message that would start a turn is refused `busy`.
|
|
25
|
+
*
|
|
26
|
+
* A piece of work is marked in the store while a turn of it hasn't ended, and a thread while no
|
|
27
|
+
* work follows it and it holds messages. A looper that closes under a turn or a decision, or goes
|
|
28
|
+
* without closing, leaves the mark, and `resume` on the same store goes on with it, once. A turn
|
|
29
|
+
* that ends, fails or is stopped leaves no mark; a thread the main loop answered, couldn't place,
|
|
30
|
+
* or was stopped over, waits for its next message, and the main loop is put what it holds.
|
|
16
31
|
*/
|
|
32
|
+
import { z } from "zod";
|
|
17
33
|
import { createLoop } from "./loop.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
34
|
+
import { createMain, stopWord, TOLD, titled } from "./main.js";
|
|
35
|
+
import { createRecord, createThread, head, messageOf, READS, userEntry, } from "./record.js";
|
|
36
|
+
import { codeOf, Refusal, } from "./types.js";
|
|
37
|
+
/** The turns that run at once, not counting the one more for work already begun. */
|
|
21
38
|
export const PLACES = 4;
|
|
22
39
|
/** The turns that may wait for a place at once. */
|
|
23
40
|
export const WAITING = 16;
|
|
41
|
+
/**
|
|
42
|
+
* What is kept of a piece of work: its title, the threads it follows, and once a turn of it has
|
|
43
|
+
* ended, when that was, the head of what it last said, and whether that turn failed.
|
|
44
|
+
*/
|
|
45
|
+
const Work = z.object({
|
|
46
|
+
title: z.string(),
|
|
47
|
+
follows: z.array(z.string()).min(1),
|
|
48
|
+
at: z.string().optional(),
|
|
49
|
+
last: z.string().optional(),
|
|
50
|
+
failed: z.literal(true).optional(),
|
|
51
|
+
});
|
|
24
52
|
/** Where a thread's record is kept in the looper's store. */
|
|
25
53
|
const recordOf = (thread) => `thread/${encodeURIComponent(thread)}/`;
|
|
26
|
-
/** Where a
|
|
54
|
+
/** Where a piece of work is marked while a turn of it hasn't ended. */
|
|
27
55
|
const UNFINISHED = "unfinished/";
|
|
56
|
+
/**
|
|
57
|
+
* Where a thread is marked while it holds messages and no piece of work has taken them: `true`
|
|
58
|
+
* while the main loop is still to decide on it, or work to take them, so the next start goes on
|
|
59
|
+
* with it, and `false` once the main loop has answered it, couldn't, or was stopped, when the
|
|
60
|
+
* thread's next message asks.
|
|
61
|
+
*/
|
|
62
|
+
const UNROUTED = "unrouted/";
|
|
63
|
+
/** Where what is kept of a piece of work is kept. */
|
|
64
|
+
const WORKS = "work/";
|
|
65
|
+
const running = (status) => status.state === "thinking" || status.state === "tool";
|
|
28
66
|
/** The store's keys under `prefix`, as a store of its own. */
|
|
29
67
|
function under(store, prefix) {
|
|
30
68
|
return {
|
|
@@ -50,18 +88,52 @@ function under(store, prefix) {
|
|
|
50
88
|
*/
|
|
51
89
|
export function createLooper(options) {
|
|
52
90
|
const { store, out, log } = options;
|
|
91
|
+
/** The pieces of work that have a loop here, by id. */
|
|
92
|
+
const works = new Map();
|
|
53
93
|
const threads = new Map();
|
|
94
|
+
/** A thread's messages, read when first asked for. */
|
|
95
|
+
const threadOf = (name) => {
|
|
96
|
+
const found = threads.get(name) ?? createThread(under(store, recordOf(name)), log);
|
|
97
|
+
threads.set(name, found);
|
|
98
|
+
return found;
|
|
99
|
+
};
|
|
100
|
+
/** What is kept of each piece of work, and the work each thread has. */
|
|
101
|
+
const kept = new Map();
|
|
102
|
+
const followed = new Map();
|
|
103
|
+
/**
|
|
104
|
+
* The threads that are marked, since no work has taken what they hold, each with what its mark
|
|
105
|
+
* says: whether the next start goes on with it.
|
|
106
|
+
*/
|
|
107
|
+
const marks = new Map();
|
|
108
|
+
/** The marked threads in which a message was kept since the main loop last read them. */
|
|
109
|
+
const unread = new Set();
|
|
110
|
+
const markOf = (thread) => UNROUTED + encodeURIComponent(thread);
|
|
111
|
+
/** Mark a thread: `again` is whether the next start goes on with it. */
|
|
112
|
+
const wait = (thread, again) => {
|
|
113
|
+
marks.set(thread, again);
|
|
114
|
+
return store.set({ key: markOf(thread), value: again });
|
|
115
|
+
};
|
|
116
|
+
/** Take a thread's mark off: a piece of work has what it holds, or it holds nothing. */
|
|
117
|
+
const settle = async (thread) => {
|
|
118
|
+
marks.delete(thread);
|
|
119
|
+
unread.delete(thread);
|
|
120
|
+
await store
|
|
121
|
+
.delete({ key: markOf(thread) })
|
|
122
|
+
.catch((error) => log.warn("A thread's mark couldn't be taken off.", { thread, error: String(error) }));
|
|
123
|
+
};
|
|
124
|
+
/** The threads being decided or given now; each settles when that has ended. */
|
|
125
|
+
const routes = new Map();
|
|
54
126
|
/** Set while it is stopped: no turn starts until it is started. */
|
|
55
127
|
let stopped = options.stopped ?? false;
|
|
56
128
|
/** Set when it closes: no turn starts after. */
|
|
57
129
|
let closed = false;
|
|
58
|
-
/**
|
|
59
|
-
let
|
|
60
|
-
/** Refuse a turn that can't start now,
|
|
61
|
-
const mayStart = () => {
|
|
130
|
+
/** Counts the stops and the close: what was asked before one starts nothing after it. */
|
|
131
|
+
let cuts = 0;
|
|
132
|
+
/** Refuse a turn that can't start now, or was asked for before a stop, whatever its thread. */
|
|
133
|
+
const mayStart = (at = cuts) => {
|
|
62
134
|
if (closed)
|
|
63
135
|
throw new Refusal("unavailable", "This agent is closing.");
|
|
64
|
-
if (stopped)
|
|
136
|
+
if (stopped || at !== cuts)
|
|
65
137
|
throw new Refusal("busy", "It is stopped. Start it to go on.");
|
|
66
138
|
};
|
|
67
139
|
/** Messages being recorded now, each about to start a turn: stopping waits for them. */
|
|
@@ -71,6 +143,15 @@ export function createLooper(options) {
|
|
|
71
143
|
starting.add(done);
|
|
72
144
|
return call;
|
|
73
145
|
};
|
|
146
|
+
/** Whether anything is at work, as it was last said. */
|
|
147
|
+
let working = false;
|
|
148
|
+
const tally = () => {
|
|
149
|
+
const now = routes.size > 0 || [...works.values()].some(({ loop }) => running(loop.state()));
|
|
150
|
+
if (now === working)
|
|
151
|
+
return;
|
|
152
|
+
working = now;
|
|
153
|
+
out.working(now);
|
|
154
|
+
};
|
|
74
155
|
/** The turns that have a place, and those waiting for one, first come first. */
|
|
75
156
|
const placed = new Set();
|
|
76
157
|
const waiting = [];
|
|
@@ -107,22 +188,82 @@ export function createLooper(options) {
|
|
|
107
188
|
throw new Refusal("busy", "Too many turns are waiting. Wait for one to start.");
|
|
108
189
|
return place;
|
|
109
190
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
191
|
+
/** What the store holds of the work the main loop placed, and of what waits: read once. */
|
|
192
|
+
let learned;
|
|
193
|
+
const keysUnder = async (prefix) => {
|
|
194
|
+
const keys = [];
|
|
195
|
+
let after;
|
|
196
|
+
do {
|
|
197
|
+
const page = await store.list({ prefix, ...(after ? { after } : {}) });
|
|
198
|
+
keys.push(...page.keys);
|
|
199
|
+
after = page.next ?? undefined;
|
|
200
|
+
} while (after);
|
|
201
|
+
return keys;
|
|
202
|
+
};
|
|
203
|
+
const learn = () => {
|
|
204
|
+
if (learned)
|
|
205
|
+
return learned;
|
|
206
|
+
/** What the store holds under `prefix`, a few at a time, each given with its key's last part. */
|
|
207
|
+
const read = async (prefix, each) => {
|
|
208
|
+
const keys = await keysUnder(prefix);
|
|
209
|
+
for (let at = 0; at < keys.length; at += READS) {
|
|
210
|
+
const some = keys.slice(at, at + READS);
|
|
211
|
+
const values = await Promise.all(some.map((key) => store.get({ key })));
|
|
212
|
+
for (const [i, key] of some.entries())
|
|
213
|
+
each(decodeURIComponent(key.slice(prefix.length)), values[i]?.value);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const attempt = (async () => {
|
|
217
|
+
await read(WORKS, (id, value) => {
|
|
218
|
+
const work = Work.safeParse(value);
|
|
219
|
+
if (!work.success) {
|
|
220
|
+
log.warn("A piece of work's record didn't read back; it is left out.", { id });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
kept.set(id, work.data);
|
|
224
|
+
for (const name of work.data.follows)
|
|
225
|
+
followed.set(name, id);
|
|
226
|
+
});
|
|
227
|
+
await read(UNROUTED, (thread, value) => void marks.set(thread, value === true));
|
|
228
|
+
})();
|
|
229
|
+
learned = attempt;
|
|
230
|
+
attempt.catch(() => {
|
|
231
|
+
if (learned === attempt)
|
|
232
|
+
learned = undefined;
|
|
233
|
+
});
|
|
234
|
+
return attempt;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* The work a thread is followed by, if any: the one the main loop gave it, or its own when it
|
|
238
|
+
* holds messages and isn't marked.
|
|
239
|
+
*/
|
|
240
|
+
const workOf = async (thread) => {
|
|
241
|
+
await learn();
|
|
242
|
+
const work = followed.get(thread);
|
|
243
|
+
if (work !== undefined)
|
|
244
|
+
return work;
|
|
245
|
+
if (marks.has(thread))
|
|
246
|
+
return undefined;
|
|
247
|
+
return (await threadOf(thread).load()).records.length > 0 ? thread : undefined;
|
|
248
|
+
};
|
|
249
|
+
const open = (id) => {
|
|
250
|
+
const record = createRecord({
|
|
251
|
+
id,
|
|
252
|
+
follows: () => kept.get(id)?.follows ?? [id],
|
|
253
|
+
thread: threadOf,
|
|
254
|
+
});
|
|
255
|
+
const key = UNFINISHED + encodeURIComponent(id);
|
|
113
256
|
const held = {
|
|
114
257
|
record,
|
|
115
|
-
/** What is being asked of its loop now: messages, going on. */
|
|
116
|
-
uses: 0,
|
|
117
258
|
/** Whether a turn of it is running, as its mark was last asked to say. */
|
|
118
259
|
marked: false,
|
|
119
|
-
/**
|
|
260
|
+
/** The writes of its mark and of its record, one after another. */
|
|
120
261
|
writes: Promise.resolve(),
|
|
121
262
|
/** Keep its mark, or take it off, after any write before. */
|
|
122
263
|
mark(on) {
|
|
123
264
|
held.writes = held.writes
|
|
124
265
|
.then(() => (on ? store.set({ key, value: true }) : store.delete({ key })))
|
|
125
|
-
.then(() => { }, (error) => log.error("A
|
|
266
|
+
.then(() => { }, (error) => log.error("A work's mark couldn't be written.", { work: id, error: String(error) }));
|
|
126
267
|
},
|
|
127
268
|
loop: createLoop({
|
|
128
269
|
request: options.request,
|
|
@@ -130,19 +271,22 @@ export function createLooper(options) {
|
|
|
130
271
|
enter,
|
|
131
272
|
record,
|
|
132
273
|
out: {
|
|
133
|
-
message:
|
|
134
|
-
delta:
|
|
274
|
+
message: out.message,
|
|
275
|
+
delta: out.delta,
|
|
276
|
+
failed: out.failed,
|
|
135
277
|
status(status) {
|
|
136
|
-
const
|
|
278
|
+
const on = running(status);
|
|
137
279
|
// A turn that starts is marked, but one that goes on from a cut-off has that mark
|
|
138
280
|
// taken off: it goes on once. A turn that ends takes its mark off, but one cut by the
|
|
139
281
|
// looper closing keeps it, for whoever opens the store next.
|
|
140
|
-
if (
|
|
282
|
+
if (on && !held.marked)
|
|
141
283
|
held.mark(!held.loop.resumed());
|
|
142
|
-
else if (!
|
|
284
|
+
else if (!on && held.marked && !closed) {
|
|
143
285
|
held.mark(false);
|
|
144
|
-
|
|
145
|
-
|
|
286
|
+
ended(id, status.state === "error");
|
|
287
|
+
}
|
|
288
|
+
held.marked = on;
|
|
289
|
+
tally();
|
|
146
290
|
},
|
|
147
291
|
},
|
|
148
292
|
log,
|
|
@@ -150,66 +294,344 @@ export function createLooper(options) {
|
|
|
150
294
|
};
|
|
151
295
|
return held;
|
|
152
296
|
};
|
|
297
|
+
const of = (id) => {
|
|
298
|
+
const found = works.get(id) ?? open(id);
|
|
299
|
+
works.set(id, found);
|
|
300
|
+
return found;
|
|
301
|
+
};
|
|
302
|
+
/** A work's title: the main loop's, or its opening line for work begun before it had one. */
|
|
303
|
+
const titleOf = async (id) => kept.get(id)?.title || titled(await of(id).record.opening());
|
|
304
|
+
/** Keep a work's record as it stands when its turn to be written comes. */
|
|
305
|
+
const save = (id) => {
|
|
306
|
+
const work = of(id);
|
|
307
|
+
const written = work.writes.then(async () => {
|
|
308
|
+
const record = kept.get(id);
|
|
309
|
+
if (record)
|
|
310
|
+
await store.set({ key: WORKS + encodeURIComponent(id), value: record });
|
|
311
|
+
});
|
|
312
|
+
work.writes = written.then(() => { }, () => { });
|
|
313
|
+
return written;
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* A turn of a piece of work has ended: keep when, the head of what it last said, and whether it
|
|
317
|
+
* failed, for the main loop to tell of it.
|
|
318
|
+
*/
|
|
319
|
+
const ended = (id, failed) => {
|
|
320
|
+
const work = of(id);
|
|
321
|
+
work.writes = work.writes
|
|
322
|
+
.then(async () => {
|
|
323
|
+
const last = await work.record.last();
|
|
324
|
+
const title = await titleOf(id);
|
|
325
|
+
// Read after the waits above: a decision may have kept that it follows a thread too.
|
|
326
|
+
const { failed: _, ...was } = kept.get(id) ?? { title, follows: [id] };
|
|
327
|
+
kept.set(id, {
|
|
328
|
+
...was,
|
|
329
|
+
at: new Date().toISOString(),
|
|
330
|
+
...(last ? { last: head(last.replace(/\s+/g, " ").trim(), TOLD.last) } : {}),
|
|
331
|
+
...(failed ? { failed: true } : {}),
|
|
332
|
+
});
|
|
333
|
+
await store.set({ key: WORKS + encodeURIComponent(id), value: kept.get(id) });
|
|
334
|
+
})
|
|
335
|
+
.catch((error) => log.error("A work's record couldn't be written.", { work: id, error: String(error) }));
|
|
336
|
+
};
|
|
337
|
+
/** Ask something of a piece of work's loop. */
|
|
338
|
+
const using = (id, ask) => started(ask(of(id).loop));
|
|
339
|
+
/** Work given something as the looper closed, its turn cut before it began: marked like one. */
|
|
340
|
+
const leftAtClose = (id) => {
|
|
341
|
+
const work = works.get(id);
|
|
342
|
+
if (closed && work && !work.marked)
|
|
343
|
+
work.mark(true);
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* What is asked of a thread whose work has to be found, or is being given it, taken one at a
|
|
347
|
+
* time in the order it was asked; and how much is still to be taken.
|
|
348
|
+
*/
|
|
349
|
+
const lines = new Map();
|
|
350
|
+
const inLine = (thread, take) => {
|
|
351
|
+
const line = lines.get(thread) ?? { last: Promise.resolve(), waiting: 0 };
|
|
352
|
+
lines.set(thread, line);
|
|
353
|
+
line.waiting++;
|
|
354
|
+
const taken = line.last.then(take).finally(() => void line.waiting--);
|
|
355
|
+
line.last = taken.catch(() => { });
|
|
356
|
+
return taken;
|
|
357
|
+
};
|
|
358
|
+
const main = createMain({ model: options.model, request: options.request, log });
|
|
359
|
+
/** The work there is, as the main loop is told of it: in a turn first, then the latest at rest. */
|
|
360
|
+
const listed = async () => {
|
|
361
|
+
const turnOf = (id) => works.get(id)?.loop.turn();
|
|
362
|
+
// Work that has a record, and work in a turn that has none yet.
|
|
363
|
+
const ids = new Set([
|
|
364
|
+
...kept.keys(),
|
|
365
|
+
...[...works.keys()].filter((id) => turnOf(id) !== undefined),
|
|
366
|
+
]);
|
|
367
|
+
const all = await Promise.all([...ids].map(async (id) => {
|
|
368
|
+
const title = await titleOf(id);
|
|
369
|
+
return {
|
|
370
|
+
id,
|
|
371
|
+
title,
|
|
372
|
+
turn: turnOf(id),
|
|
373
|
+
last: kept.get(id)?.last,
|
|
374
|
+
failed: kept.get(id)?.failed,
|
|
375
|
+
at: kept.get(id)?.at ?? "",
|
|
376
|
+
};
|
|
377
|
+
}));
|
|
378
|
+
return all
|
|
379
|
+
.filter(({ title }) => title !== "")
|
|
380
|
+
.sort((a, b) => Number(b.turn !== undefined) - Number(a.turn !== undefined) ||
|
|
381
|
+
b.at.localeCompare(a.at) ||
|
|
382
|
+
a.id.localeCompare(b.id))
|
|
383
|
+
.slice(0, TOLD.works)
|
|
384
|
+
.map(({ at: _, ...work }) => work);
|
|
385
|
+
};
|
|
153
386
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
387
|
+
* Keep that a piece of work follows a thread too, under `title` if it is new: kept before the
|
|
388
|
+
* thread's messages are taken, so a start in between finds whose they are.
|
|
156
389
|
*/
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
390
|
+
const follow = async (thread, work, title) => {
|
|
391
|
+
const named = title ?? (await titleOf(work));
|
|
392
|
+
const was = kept.get(work);
|
|
393
|
+
kept.set(work, {
|
|
394
|
+
...was,
|
|
395
|
+
title: named,
|
|
396
|
+
follows: [...new Set([...(was?.follows ?? [work]), thread])],
|
|
397
|
+
});
|
|
161
398
|
try {
|
|
162
|
-
|
|
399
|
+
await save(work);
|
|
163
400
|
}
|
|
164
401
|
catch (error) {
|
|
165
|
-
|
|
166
|
-
|
|
402
|
+
// Not kept: it follows what it did, with whatever a turn's end has written since.
|
|
403
|
+
const now = kept.get(work);
|
|
404
|
+
if (was && now)
|
|
405
|
+
kept.set(work, { ...now, title: was.title, follows: was.follows });
|
|
406
|
+
else
|
|
407
|
+
kept.delete(work);
|
|
167
408
|
throw error;
|
|
168
409
|
}
|
|
410
|
+
return work;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Have the main loop decide on a thread nothing follows, and keep what it says: an answer of
|
|
414
|
+
* its own, which is the thread's next message, and whose what was said there is, which is new
|
|
415
|
+
* work of the thread's own or work there is, which then follows the thread too; or cut the turn
|
|
416
|
+
* it says to stop. It gives the work, or none when the main loop only answered or stopped, when
|
|
417
|
+
* nothing waits for an answer, or once `cut` says the looper stopped or closed.
|
|
418
|
+
*/
|
|
419
|
+
const decide = async (thread, cut) => {
|
|
420
|
+
// Read in the thread's line: a message being kept there now is kept first.
|
|
421
|
+
const read = await inLine(thread, async () => {
|
|
422
|
+
unread.delete(thread);
|
|
423
|
+
const read = await threadOf(thread).said();
|
|
424
|
+
// Marked before a message that was never kept: nothing waits.
|
|
425
|
+
if (read.said.length === 0)
|
|
426
|
+
await settle(thread);
|
|
427
|
+
return read;
|
|
428
|
+
});
|
|
429
|
+
// Nothing waits for an answer: nothing was said, or the main loop has answered the newest.
|
|
430
|
+
if (read.said.at(-1)?.role !== "user")
|
|
431
|
+
return undefined;
|
|
432
|
+
const there = await listed();
|
|
433
|
+
if (cut())
|
|
434
|
+
return undefined;
|
|
435
|
+
const replyId = crypto.randomUUID();
|
|
436
|
+
const decided = await main.decide(read.said, there, (text) => out.delta(thread, replyId, text));
|
|
437
|
+
if (!decided || cut())
|
|
438
|
+
return undefined;
|
|
439
|
+
const { placed, stop } = decided;
|
|
440
|
+
// Stopping is cutting the turn it was told of, if that is still the work's turn. This thread
|
|
441
|
+
// is answered either way, as the work is not its own: with what the main loop wrote, or with
|
|
442
|
+
// whether a turn was cut.
|
|
443
|
+
const cutTurn = stop?.turn !== undefined && (await works.get(stop.work)?.loop.cancel(stop.turn)) === true;
|
|
444
|
+
const answer = decided.answer ?? (stop ? stopWord(cutTurn) : undefined);
|
|
445
|
+
// Whose it is is kept first: an answer beside it is never all that is left of a decision.
|
|
446
|
+
const work = placed &&
|
|
447
|
+
("work" in placed
|
|
448
|
+
? await follow(thread, placed.work)
|
|
449
|
+
: await follow(thread, thread, placed.title));
|
|
450
|
+
try {
|
|
451
|
+
if (answer !== undefined)
|
|
452
|
+
// What it said itself is the thread's, kept before any work takes the thread. With
|
|
453
|
+
// nothing placed and nothing said meanwhile, the thread waits for its next message, not
|
|
454
|
+
// for the next start; a message said meanwhile is still to be decided on.
|
|
455
|
+
await inLine(thread, async () => {
|
|
456
|
+
const { entry, late } = await threadOf(thread).answer(answer, read.upTo, replyId);
|
|
457
|
+
out.message(thread, messageOf(entry));
|
|
458
|
+
if (work === undefined && !late)
|
|
459
|
+
await wait(thread, false).catch((error) => log.warn("A thread's mark couldn't be written.", { thread, error: String(error) }));
|
|
460
|
+
});
|
|
461
|
+
}
|
|
169
462
|
finally {
|
|
170
|
-
|
|
463
|
+
// Only now is the thread its work's to be sent in: after what the main loop said there,
|
|
464
|
+
// or couldn't keep. Whose it is stands either way, as it is kept.
|
|
465
|
+
if (work !== undefined)
|
|
466
|
+
followed.set(thread, work);
|
|
171
467
|
}
|
|
172
|
-
|
|
173
|
-
const of = (thread) => {
|
|
174
|
-
const found = threads.get(thread) ?? open(thread);
|
|
175
|
-
threads.set(thread, found);
|
|
176
|
-
return found;
|
|
468
|
+
return work;
|
|
177
469
|
};
|
|
178
470
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* One that can't go on now keeps its mark for the next time. A stopped looper goes on with
|
|
182
|
-
* none, and its marks go: so does one stopped before it was asked to. Whoever hosts the looper calls this when a request can be made;
|
|
183
|
-
* once the marks have been read, a later call does nothing.
|
|
471
|
+
* Decisions are made one at a time, each to its end, so each knows what the last began. What
|
|
472
|
+
* the main loop hears of a message that joined a turn takes its place in the same line.
|
|
184
473
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
474
|
+
let decided = Promise.resolve();
|
|
475
|
+
/**
|
|
476
|
+
* Give the messages waiting in a thread to a piece of work: the one it has, or the one the main
|
|
477
|
+
* loop says. One at a time for a thread, on all that waits in it; what is said there meanwhile
|
|
478
|
+
* goes with them, or is decided on when that has ended without taking it. Once the looper has
|
|
479
|
+
* stopped or closed since this was asked, nothing starts, and what waits goes on waiting.
|
|
480
|
+
*/
|
|
481
|
+
const route = (thread) => {
|
|
482
|
+
if (stopped || closed)
|
|
187
483
|
return;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
484
|
+
if (routes.has(thread))
|
|
485
|
+
return;
|
|
486
|
+
const at = cuts;
|
|
487
|
+
const cut = () => at !== cuts;
|
|
488
|
+
const routing = decided
|
|
489
|
+
.then(async () => {
|
|
490
|
+
if (cut())
|
|
491
|
+
return;
|
|
492
|
+
const work = followed.get(thread) ?? (await decide(thread, cut));
|
|
493
|
+
if (work === undefined)
|
|
494
|
+
return;
|
|
495
|
+
// In the thread's line: a message being kept there now is kept first, and goes with them.
|
|
496
|
+
await inLine(thread, async () => {
|
|
497
|
+
if (cut())
|
|
498
|
+
return;
|
|
499
|
+
// All that is kept there by now is given: none of it is still to be decided on.
|
|
500
|
+
unread.delete(thread);
|
|
501
|
+
if (!(await using(work, (loop) => loop.adopt(thread)))) {
|
|
502
|
+
// Its work was told to stop first: they wait for the thread's next message.
|
|
503
|
+
if (!cut())
|
|
504
|
+
await wait(thread, false);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
leftAtClose(work);
|
|
508
|
+
await settle(thread);
|
|
509
|
+
});
|
|
510
|
+
})
|
|
511
|
+
.catch(async (error) => {
|
|
512
|
+
if (closed)
|
|
513
|
+
return;
|
|
514
|
+
log.error("A message got no answer.", { thread, error: String(error) });
|
|
515
|
+
out.failed(thread, codeOf(error));
|
|
516
|
+
// Still waiting: for the thread's next message, not for the next start; unless one was
|
|
517
|
+
// kept meanwhile, which is decided on next. In the thread's line, after one being kept.
|
|
518
|
+
await inLine(thread, async () => {
|
|
519
|
+
if (marks.has(thread) && !unread.has(thread))
|
|
520
|
+
await wait(thread, false);
|
|
521
|
+
}).catch(() => { });
|
|
522
|
+
})
|
|
523
|
+
.finally(() => {
|
|
524
|
+
routes.delete(thread);
|
|
525
|
+
// Not once it has stopped or closed since: then what waits goes on waiting.
|
|
526
|
+
if (unread.has(thread) && !cut() && marks.has(thread))
|
|
527
|
+
route(thread);
|
|
528
|
+
tally();
|
|
529
|
+
});
|
|
530
|
+
decided = routing;
|
|
531
|
+
routes.set(thread, routing);
|
|
532
|
+
tally();
|
|
533
|
+
};
|
|
534
|
+
/** Keep a message in a thread nothing follows, and have it routed, unless a stop came since `at`. */
|
|
535
|
+
const intake = async (thread, text, at) => {
|
|
536
|
+
const was = marks.get(thread);
|
|
537
|
+
const entry = await (async () => {
|
|
538
|
+
await wait(thread, true);
|
|
539
|
+
const into = threadOf(thread);
|
|
540
|
+
const { record, stored } = into.push(await into.load(), userEntry(text));
|
|
541
|
+
await stored;
|
|
542
|
+
unread.add(thread);
|
|
543
|
+
return record.entry;
|
|
544
|
+
})().catch(async (error) => {
|
|
545
|
+
// Not kept: the thread's mark is as it was before it.
|
|
546
|
+
if (was === undefined)
|
|
547
|
+
await settle(thread);
|
|
548
|
+
else
|
|
549
|
+
await wait(thread, was).catch(() => { });
|
|
200
550
|
throw error;
|
|
201
|
-
}
|
|
551
|
+
});
|
|
552
|
+
out.message(thread, messageOf(entry));
|
|
553
|
+
// Stopped or closed while it was kept, it waits: for the next message, or the next start.
|
|
554
|
+
if (at === cuts)
|
|
555
|
+
route(thread);
|
|
556
|
+
return { messageId: entry.id };
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* Tell the main loop of a message said in `thread`, which `work` follows, that joined `turn`
|
|
560
|
+
* of it: it may only stop that turn, and then what it says is kept in the thread as the work's
|
|
561
|
+
* to read next. This is beside the message's path, after the decisions asked before it, and
|
|
562
|
+
* stops nothing once that turn has ended. A message the work takes with a thread, said before
|
|
563
|
+
* it followed that thread, is not told of. It is not kept: one cut off by a stop, a close or a restart is not
|
|
564
|
+
* heard, and the worker has the message all the same.
|
|
565
|
+
*/
|
|
566
|
+
const hear = (thread, work, text, turn) => {
|
|
567
|
+
if (stopped || closed)
|
|
568
|
+
return;
|
|
569
|
+
const at = cuts;
|
|
570
|
+
const cut = () => at !== cuts;
|
|
571
|
+
decided = decided
|
|
572
|
+
.then(async () => {
|
|
573
|
+
const { loop } = of(work);
|
|
574
|
+
if (cut() || loop.turn() !== turn)
|
|
575
|
+
return;
|
|
576
|
+
const title = await titleOf(work);
|
|
577
|
+
if (cut() || loop.turn() !== turn)
|
|
578
|
+
return;
|
|
579
|
+
const say = await main.hear(text, { id: work, title, turn });
|
|
580
|
+
if (say === undefined || cut())
|
|
581
|
+
return;
|
|
582
|
+
// A line the store refuses is the thread's to be told of: the turn is cut all the same.
|
|
583
|
+
await loop.cancel(turn, { text: say, thread }).catch((error) => {
|
|
584
|
+
log.error("What the main loop said as it stopped a turn couldn't be kept.", {
|
|
585
|
+
thread,
|
|
586
|
+
error: String(error),
|
|
587
|
+
});
|
|
588
|
+
out.failed(thread, codeOf(error));
|
|
589
|
+
});
|
|
590
|
+
})
|
|
591
|
+
.catch((error) => log.warn("A message said to work in a turn wasn't heard.", {
|
|
592
|
+
thread,
|
|
593
|
+
error: String(error),
|
|
594
|
+
}));
|
|
595
|
+
};
|
|
596
|
+
/** Give a message to the work its thread is followed by. */
|
|
597
|
+
const give = async (work, thread, text) => {
|
|
598
|
+
const { turn, ...sent } = await using(work, (loop) => loop.send(text, thread));
|
|
599
|
+
// One that joined a turn is that turn's; and the main loop hears of it, to stop the turn if
|
|
600
|
+
// that is what it asks.
|
|
601
|
+
if (turn !== undefined)
|
|
602
|
+
hear(thread, work, text, turn);
|
|
603
|
+
leftAtClose(work);
|
|
604
|
+
// What waited there, its work decided but not begun, went with it.
|
|
605
|
+
if (marks.has(thread) && !routes.has(thread))
|
|
606
|
+
await settle(thread);
|
|
607
|
+
return sent;
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* Go on with what was cut off when this store was last used, by a looper closing under it or
|
|
611
|
+
* going without closing: each turn that hadn't ended, once, waiting for a place like any turn;
|
|
612
|
+
* and each thread whose messages were still to be given to a piece of work, once too. A turn
|
|
613
|
+
* that can't go on now keeps its mark for the next time. A stopped looper goes on with none:
|
|
614
|
+
* its turns' marks go, and its waiting messages wait for their thread's next. Whoever hosts the
|
|
615
|
+
* looper calls this when a request can be made; once the marks have been read, a later call
|
|
616
|
+
* does nothing. `drop` goes on with nothing, as a stopped looper does.
|
|
617
|
+
*/
|
|
618
|
+
const goOn = async (drop) => {
|
|
619
|
+
const at = cuts;
|
|
620
|
+
/** Whether anything may still go on: not once it has stopped or closed since this began. */
|
|
621
|
+
const going = () => !drop && !stopped && at === cuts;
|
|
622
|
+
await learn();
|
|
623
|
+
const keys = await keysUnder(UNFINISHED);
|
|
202
624
|
for (const key of keys) {
|
|
203
625
|
// Closed meanwhile: what is left stays marked, for whoever opens the store next.
|
|
204
626
|
if (closed)
|
|
205
627
|
return;
|
|
206
|
-
const
|
|
207
|
-
const held = of(
|
|
628
|
+
const id = decodeURIComponent(key.slice(UNFINISHED.length));
|
|
629
|
+
const held = of(id);
|
|
208
630
|
try {
|
|
209
|
-
if (
|
|
210
|
-
await using(
|
|
211
|
-
//
|
|
212
|
-
//
|
|
631
|
+
if (going())
|
|
632
|
+
await using(id, (loop) => loop.resume());
|
|
633
|
+
// Work in a turn now keeps the mark as that turn's, and a looper that has closed leaves
|
|
634
|
+
// it. Otherwise nothing goes on, and it goes.
|
|
213
635
|
if (!held.marked && !closed)
|
|
214
636
|
held.mark(false);
|
|
215
637
|
}
|
|
@@ -217,65 +639,123 @@ export function createLooper(options) {
|
|
|
217
639
|
log.warn("A turn that was cut off didn't go on.", { key, error: String(error) });
|
|
218
640
|
}
|
|
219
641
|
}
|
|
220
|
-
|
|
642
|
+
for (const [thread, again] of [...marks]) {
|
|
643
|
+
// Closed meanwhile: what waits keeps its mark, for whoever opens the store next.
|
|
644
|
+
if (closed)
|
|
645
|
+
return;
|
|
646
|
+
// Only what was still to go on, and once: whatever comes of it, the next start doesn't.
|
|
647
|
+
if (!again)
|
|
648
|
+
continue;
|
|
649
|
+
try {
|
|
650
|
+
// In the thread's line, so its mark is written apart from a message kept there.
|
|
651
|
+
await inLine(thread, async () => {
|
|
652
|
+
if (routes.has(thread) || marks.get(thread) !== true || closed)
|
|
653
|
+
return;
|
|
654
|
+
if (going())
|
|
655
|
+
route(thread);
|
|
656
|
+
await wait(thread, false);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
log.warn("A message that was waiting wasn't placed.", { thread, error: String(error) });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
/** What was cut off is looked for once; a look that couldn't read the store is made again. */
|
|
665
|
+
let looked;
|
|
666
|
+
const resume = (drop = false) => {
|
|
667
|
+
if (closed)
|
|
668
|
+
return Promise.resolve();
|
|
669
|
+
if (!looked) {
|
|
670
|
+
const look = goOn(drop);
|
|
671
|
+
looked = look;
|
|
672
|
+
look.catch(() => {
|
|
673
|
+
if (looked === look)
|
|
674
|
+
looked = undefined;
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
return looked;
|
|
678
|
+
};
|
|
221
679
|
return {
|
|
222
680
|
/**
|
|
223
|
-
* Say a message in a thread. It is recorded and said at once.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
681
|
+
* Say a message in a thread. It is recorded and said at once. In a thread a piece of work
|
|
682
|
+
* follows it is that work's: with no turn of it running it starts one, which waits for a place
|
|
683
|
+
* if none is free, and a turn running or waiting takes it at its next model request. In a
|
|
684
|
+
* thread nothing follows, the main loop answers it or says whose it is first. Refused `busy`
|
|
685
|
+
* while `WAITING` turns already wait and it would start one more, or while the looper is
|
|
686
|
+
* stopped; and `unavailable` once it has closed.
|
|
227
687
|
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
688
|
+
send(thread, text) {
|
|
689
|
+
try {
|
|
690
|
+
mayStart();
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
return Promise.reject(error);
|
|
694
|
+
}
|
|
695
|
+
const at = cuts;
|
|
696
|
+
// Work known here is asked at once, so a stop that comes after this cuts it too.
|
|
697
|
+
const known = followed.get(thread) ?? (!marks.has(thread) && works.has(thread) ? thread : undefined);
|
|
698
|
+
if (known !== undefined && !lines.get(thread)?.waiting)
|
|
699
|
+
return give(known, thread, text);
|
|
700
|
+
return started(inLine(thread, async () => {
|
|
701
|
+
const work = await workOf(thread);
|
|
702
|
+
// Stopped or closed while its work was found: nothing of it is kept.
|
|
703
|
+
mayStart(at);
|
|
704
|
+
return work === undefined ? intake(thread, text, at) : give(work, thread, text);
|
|
705
|
+
}));
|
|
236
706
|
},
|
|
237
|
-
/** Stop a
|
|
238
|
-
cancel: async (
|
|
239
|
-
/** What a
|
|
240
|
-
state: (
|
|
707
|
+
/** Stop a piece of work's turn now. A call already running finishes, and its result is kept. */
|
|
708
|
+
cancel: async (work) => void (await works.get(work)?.loop.cancel()),
|
|
709
|
+
/** What a piece of work's loop is doing. */
|
|
710
|
+
state: (work) => works.get(work)?.loop.state() ?? { state: "idle" },
|
|
241
711
|
/** A thread's messages before `before`, oldest first, as many as fit `limit`. */
|
|
242
|
-
page: (thread, before, limit) =>
|
|
712
|
+
page: (thread, before, limit) => threadOf(thread).page(before, limit),
|
|
243
713
|
/**
|
|
244
|
-
* Stop: every turn is cut, running or waiting,
|
|
245
|
-
* running finishes first, so this answers when nothing more will
|
|
246
|
-
* as they are, to be read.
|
|
714
|
+
* Stop: every turn is cut, running or waiting, the main loop is cut too, and nothing starts
|
|
715
|
+
* until `start`. A call already running finishes first, so this answers when nothing more will
|
|
716
|
+
* be written. The records stay as they are, to be read. What was cut off earlier no longer
|
|
717
|
+
* goes on, and messages waiting for a piece of work wait for their thread's next.
|
|
247
718
|
*/
|
|
248
719
|
async stop() {
|
|
249
720
|
stopped = true;
|
|
250
|
-
|
|
251
|
-
|
|
721
|
+
cuts++;
|
|
722
|
+
const loops = [...works.values()].map(({ loop }) => loop);
|
|
723
|
+
await Promise.all([...loops.map((loop) => loop.cancel()), main.cancel()]);
|
|
252
724
|
// A message being recorded as this was asked is recorded first, and a call still running ends.
|
|
253
725
|
await Promise.all(starting);
|
|
726
|
+
await decided;
|
|
254
727
|
await Promise.all(loops.map((loop) => loop.drained()));
|
|
728
|
+
await Promise.all([...works.values()].map(({ writes }) => writes));
|
|
255
729
|
// What was cut off earlier and hasn't gone on doesn't after a stop either: its marks go.
|
|
256
|
-
|
|
730
|
+
// Nor does the next start go on with what waits: a thread's next message does.
|
|
731
|
+
await resume(true)
|
|
732
|
+
.then(() => Promise.all([...marks].flatMap(([thread, again]) => (again ? [wait(thread, false)] : []))))
|
|
733
|
+
.catch((error) => log.warn("What was cut off couldn't be looked for.", { error: String(error) }));
|
|
257
734
|
},
|
|
258
735
|
/** Take messages again. The turns `stop` cut stay cut. */
|
|
259
736
|
start() {
|
|
260
737
|
stopped = false;
|
|
261
738
|
},
|
|
262
|
-
resume,
|
|
739
|
+
resume: () => resume(),
|
|
263
740
|
/**
|
|
264
|
-
* Close for good: every turn is cut, and none starts after. Each stays marked,
|
|
265
|
-
* in whoever opens the store next.
|
|
741
|
+
* Close for good: every turn is cut, and none starts after. Each stays marked, as does each
|
|
742
|
+
* thread where messages wait, for `resume` in whoever opens the store next.
|
|
266
743
|
*/
|
|
267
744
|
async close() {
|
|
268
745
|
closed = true;
|
|
269
|
-
|
|
746
|
+
cuts++;
|
|
747
|
+
await Promise.all([...[...works.values()].map(({ loop }) => loop.cancel()), main.cancel()]);
|
|
270
748
|
},
|
|
271
|
-
/** Settles when every
|
|
749
|
+
/** Settles when every piece of work's latest turn has written all it will. */
|
|
272
750
|
async settled() {
|
|
273
|
-
await
|
|
274
|
-
await Promise.all([...
|
|
751
|
+
await decided;
|
|
752
|
+
await Promise.all([...works.values()].map(({ loop }) => loop.settled()));
|
|
753
|
+
await Promise.all([...works.values()].map(({ writes }) => writes));
|
|
275
754
|
},
|
|
276
755
|
/** Text the model writes while it answers `requestId`, for the reply it becomes. */
|
|
277
756
|
delta(requestId, text) {
|
|
278
|
-
|
|
757
|
+
main.delta(requestId, text);
|
|
758
|
+
for (const { loop } of works.values())
|
|
279
759
|
loop.delta(requestId, text);
|
|
280
760
|
},
|
|
281
761
|
};
|