@agentorg/cli 0.0.8 → 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 +17 -8
- package/dist/node_modules/@agentorg/looper/dist/src/loop.js +80 -41
- package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +39 -5
- package/dist/node_modules/@agentorg/looper/dist/src/looper.js +290 -102
- package/dist/node_modules/@agentorg/looper/dist/src/main.d.ts +73 -17
- package/dist/node_modules/@agentorg/looper/dist/src/main.js +166 -77
- package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +76 -18
- package/dist/node_modules/@agentorg/looper/dist/src/record.js +190 -111
- package/dist/node_modules/@agentorg/shell/dist/app.css +255 -276
- package/dist/node_modules/@agentorg/shell/dist/app.js +144 -147
- package/dist/node_modules/assistant/dist/src/chats.d.ts +10 -5
- package/dist/node_modules/assistant/dist/src/chats.js +5 -4
- package/dist/node_modules/assistant/dist/src/engine.js +10 -4
- 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,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The looper: the loops of whoever hosts it. Messages come in on threads, which are names its host
|
|
3
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.
|
|
4
|
+
* piece of work, with a loop of its own, or, when a message needs no work, the main loop.
|
|
5
5
|
*
|
|
6
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
|
|
8
|
-
* thread's
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
12
|
+
*
|
|
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.
|
|
11
19
|
*
|
|
12
20
|
* Turns run side by side, each in a place; the main loop takes none. There are `PLACES` of them,
|
|
13
21
|
* and one more that only work already begun (it has done something) may take, so new work never
|
|
@@ -15,31 +23,44 @@
|
|
|
15
23
|
* order it came. While begun work keeps five turns running, new work goes on waiting. `WAITING`
|
|
16
24
|
* turns may wait; past that a message that would start a turn is refused `busy`.
|
|
17
25
|
*
|
|
18
|
-
* A piece of work is marked in the store while a turn of it hasn't ended, and a thread while
|
|
19
|
-
*
|
|
20
|
-
* mark, and `resume` on the same store goes on with it, once. A turn
|
|
21
|
-
* stopped leaves no mark;
|
|
22
|
-
*
|
|
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.
|
|
23
31
|
*/
|
|
32
|
+
import { z } from "zod";
|
|
24
33
|
import { createLoop } from "./loop.js";
|
|
25
|
-
import { createMain, titled } from "./main.js";
|
|
26
|
-
import { createRecord, createThread, messageOf, READS,
|
|
34
|
+
import { createMain, stopWord, TOLD, titled } from "./main.js";
|
|
35
|
+
import { createRecord, createThread, head, messageOf, READS, userEntry, } from "./record.js";
|
|
27
36
|
import { codeOf, Refusal, } from "./types.js";
|
|
28
37
|
/** The turns that run at once, not counting the one more for work already begun. */
|
|
29
38
|
export const PLACES = 4;
|
|
30
39
|
/** The turns that may wait for a place at once. */
|
|
31
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
|
+
});
|
|
32
52
|
/** Where a thread's record is kept in the looper's store. */
|
|
33
53
|
const recordOf = (thread) => `thread/${encodeURIComponent(thread)}/`;
|
|
34
54
|
/** Where a piece of work is marked while a turn of it hasn't ended. */
|
|
35
55
|
const UNFINISHED = "unfinished/";
|
|
36
56
|
/**
|
|
37
|
-
* Where a thread is marked while
|
|
38
|
-
* while
|
|
39
|
-
* or was stopped, when the
|
|
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.
|
|
40
61
|
*/
|
|
41
62
|
const UNROUTED = "unrouted/";
|
|
42
|
-
/** Where a piece of work
|
|
63
|
+
/** Where what is kept of a piece of work is kept. */
|
|
43
64
|
const WORKS = "work/";
|
|
44
65
|
const running = (status) => status.state === "thinking" || status.state === "tool";
|
|
45
66
|
/** The store's keys under `prefix`, as a store of its own. */
|
|
@@ -76,17 +97,26 @@ export function createLooper(options) {
|
|
|
76
97
|
threads.set(name, found);
|
|
77
98
|
return found;
|
|
78
99
|
};
|
|
79
|
-
/** What is kept of each piece of work
|
|
100
|
+
/** What is kept of each piece of work, and the work each thread has. */
|
|
80
101
|
const kept = new Map();
|
|
81
102
|
const followed = new Map();
|
|
82
|
-
/**
|
|
83
|
-
|
|
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();
|
|
84
110
|
const markOf = (thread) => UNROUTED + encodeURIComponent(thread);
|
|
85
|
-
/**
|
|
86
|
-
const wait = (thread, again) =>
|
|
87
|
-
|
|
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. */
|
|
88
117
|
const settle = async (thread) => {
|
|
89
|
-
|
|
118
|
+
marks.delete(thread);
|
|
119
|
+
unread.delete(thread);
|
|
90
120
|
await store
|
|
91
121
|
.delete({ key: markOf(thread) })
|
|
92
122
|
.catch((error) => log.warn("A thread's mark couldn't be taken off.", { thread, error: String(error) }));
|
|
@@ -173,28 +203,28 @@ export function createLooper(options) {
|
|
|
173
203
|
const learn = () => {
|
|
174
204
|
if (learned)
|
|
175
205
|
return learned;
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
209
|
for (let at = 0; at < keys.length; at += READS) {
|
|
179
210
|
const some = keys.slice(at, at + READS);
|
|
180
211
|
const values = await Promise.all(some.map((key) => store.get({ key })));
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const follows = Array.isArray(value?.follows)
|
|
184
|
-
? value.follows.filter((name) => typeof name === "string")
|
|
185
|
-
: [];
|
|
186
|
-
if (follows.length === 0) {
|
|
187
|
-
log.warn("A piece of work's record didn't read back; it is left out.", { key });
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const id = decodeURIComponent(key.slice(WORKS.length));
|
|
191
|
-
kept.set(id, { title: typeof value?.title === "string" ? value.title : "", follows });
|
|
192
|
-
for (const name of follows)
|
|
193
|
-
followed.set(name, id);
|
|
194
|
-
});
|
|
212
|
+
for (const [i, key] of some.entries())
|
|
213
|
+
each(decodeURIComponent(key.slice(prefix.length)), values[i]?.value);
|
|
195
214
|
}
|
|
196
|
-
|
|
197
|
-
|
|
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));
|
|
198
228
|
})();
|
|
199
229
|
learned = attempt;
|
|
200
230
|
attempt.catch(() => {
|
|
@@ -205,14 +235,14 @@ export function createLooper(options) {
|
|
|
205
235
|
};
|
|
206
236
|
/**
|
|
207
237
|
* The work a thread is followed by, if any: the one the main loop gave it, or its own when it
|
|
208
|
-
* holds messages and
|
|
238
|
+
* holds messages and isn't marked.
|
|
209
239
|
*/
|
|
210
240
|
const workOf = async (thread) => {
|
|
211
241
|
await learn();
|
|
212
242
|
const work = followed.get(thread);
|
|
213
243
|
if (work !== undefined)
|
|
214
244
|
return work;
|
|
215
|
-
if (
|
|
245
|
+
if (marks.has(thread))
|
|
216
246
|
return undefined;
|
|
217
247
|
return (await threadOf(thread).load()).records.length > 0 ? thread : undefined;
|
|
218
248
|
};
|
|
@@ -227,7 +257,7 @@ export function createLooper(options) {
|
|
|
227
257
|
record,
|
|
228
258
|
/** Whether a turn of it is running, as its mark was last asked to say. */
|
|
229
259
|
marked: false,
|
|
230
|
-
/**
|
|
260
|
+
/** The writes of its mark and of its record, one after another. */
|
|
231
261
|
writes: Promise.resolve(),
|
|
232
262
|
/** Keep its mark, or take it off, after any write before. */
|
|
233
263
|
mark(on) {
|
|
@@ -251,8 +281,10 @@ export function createLooper(options) {
|
|
|
251
281
|
// looper closing keeps it, for whoever opens the store next.
|
|
252
282
|
if (on && !held.marked)
|
|
253
283
|
held.mark(!held.loop.resumed());
|
|
254
|
-
else if (!on && held.marked && !closed)
|
|
284
|
+
else if (!on && held.marked && !closed) {
|
|
255
285
|
held.mark(false);
|
|
286
|
+
ended(id, status.state === "error");
|
|
287
|
+
}
|
|
256
288
|
held.marked = on;
|
|
257
289
|
tally();
|
|
258
290
|
},
|
|
@@ -269,6 +301,39 @@ export function createLooper(options) {
|
|
|
269
301
|
};
|
|
270
302
|
/** A work's title: the main loop's, or its opening line for work begun before it had one. */
|
|
271
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
|
+
};
|
|
272
337
|
/** Ask something of a piece of work's loop. */
|
|
273
338
|
const using = (id, ask) => started(ask(of(id).loop));
|
|
274
339
|
/** Work given something as the looper closed, its turn cut before it began: marked like one. */
|
|
@@ -290,62 +355,134 @@ export function createLooper(options) {
|
|
|
290
355
|
line.last = taken.catch(() => { });
|
|
291
356
|
return taken;
|
|
292
357
|
};
|
|
293
|
-
const main = createMain({
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
};
|
|
298
386
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
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.
|
|
389
|
+
*/
|
|
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
|
+
});
|
|
398
|
+
try {
|
|
399
|
+
await save(work);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
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);
|
|
408
|
+
throw error;
|
|
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.
|
|
302
418
|
*/
|
|
303
419
|
const decide = async (thread, cut) => {
|
|
304
420
|
// Read in the thread's line: a message being kept there now is kept first.
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
const
|
|
421
|
+
const read = await inLine(thread, async () => {
|
|
422
|
+
unread.delete(thread);
|
|
423
|
+
const read = await threadOf(thread).said();
|
|
308
424
|
// Marked before a message that was never kept: nothing waits.
|
|
309
|
-
if (said.length === 0)
|
|
425
|
+
if (read.said.length === 0)
|
|
310
426
|
await settle(thread);
|
|
311
|
-
return
|
|
427
|
+
return read;
|
|
312
428
|
});
|
|
313
|
-
|
|
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")
|
|
314
431
|
return undefined;
|
|
315
|
-
const
|
|
316
|
-
.filter(([, { loop }]) => running(loop.state()))
|
|
317
|
-
.map(async ([id]) => ({ id, title: await titleOf(id) })));
|
|
432
|
+
const there = await listed();
|
|
318
433
|
if (cut())
|
|
319
434
|
return undefined;
|
|
320
|
-
const
|
|
321
|
-
|
|
435
|
+
const replyId = crypto.randomUUID();
|
|
436
|
+
const decided = await main.decide(read.said, there, (text) => out.delta(thread, replyId, text));
|
|
437
|
+
if (!decided || cut())
|
|
322
438
|
return undefined;
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
}
|
|
462
|
+
finally {
|
|
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);
|
|
467
|
+
}
|
|
332
468
|
return work;
|
|
333
469
|
};
|
|
334
|
-
/**
|
|
470
|
+
/**
|
|
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.
|
|
473
|
+
*/
|
|
335
474
|
let decided = Promise.resolve();
|
|
336
|
-
/** The threads a message was kept in while they were being decided or given. */
|
|
337
|
-
const again = new Set();
|
|
338
475
|
/**
|
|
339
476
|
* Give the messages waiting in a thread to a piece of work: the one it has, or the one the main
|
|
340
477
|
* loop says. One at a time for a thread, on all that waits in it; what is said there meanwhile
|
|
341
|
-
* goes with them, or is
|
|
478
|
+
* goes with them, or is decided on when that has ended without taking it. Once the looper has
|
|
342
479
|
* stopped or closed since this was asked, nothing starts, and what waits goes on waiting.
|
|
343
480
|
*/
|
|
344
481
|
const route = (thread) => {
|
|
345
482
|
if (stopped || closed)
|
|
346
483
|
return;
|
|
347
484
|
if (routes.has(thread))
|
|
348
|
-
return
|
|
485
|
+
return;
|
|
349
486
|
const at = cuts;
|
|
350
487
|
const cut = () => at !== cuts;
|
|
351
488
|
const routing = decided
|
|
@@ -359,6 +496,8 @@ export function createLooper(options) {
|
|
|
359
496
|
await inLine(thread, async () => {
|
|
360
497
|
if (cut())
|
|
361
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);
|
|
362
501
|
if (!(await using(work, (loop) => loop.adopt(thread)))) {
|
|
363
502
|
// Its work was told to stop first: they wait for the thread's next message.
|
|
364
503
|
if (!cut())
|
|
@@ -374,14 +513,17 @@ export function createLooper(options) {
|
|
|
374
513
|
return;
|
|
375
514
|
log.error("A message got no answer.", { thread, error: String(error) });
|
|
376
515
|
out.failed(thread, codeOf(error));
|
|
377
|
-
// Still waiting: for the thread's next message, not for the next start
|
|
378
|
-
|
|
379
|
-
|
|
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(() => { });
|
|
380
522
|
})
|
|
381
523
|
.finally(() => {
|
|
382
524
|
routes.delete(thread);
|
|
383
525
|
// Not once it has stopped or closed since: then what waits goes on waiting.
|
|
384
|
-
if (
|
|
526
|
+
if (unread.has(thread) && !cut() && marks.has(thread))
|
|
385
527
|
route(thread);
|
|
386
528
|
tally();
|
|
387
529
|
});
|
|
@@ -391,18 +533,20 @@ export function createLooper(options) {
|
|
|
391
533
|
};
|
|
392
534
|
/** Keep a message in a thread nothing follows, and have it routed, unless a stop came since `at`. */
|
|
393
535
|
const intake = async (thread, text, at) => {
|
|
394
|
-
const
|
|
395
|
-
unrouted.add(thread);
|
|
536
|
+
const was = marks.get(thread);
|
|
396
537
|
const entry = await (async () => {
|
|
397
538
|
await wait(thread, true);
|
|
398
539
|
const into = threadOf(thread);
|
|
399
540
|
const { record, stored } = into.push(await into.load(), userEntry(text));
|
|
400
541
|
await stored;
|
|
542
|
+
unread.add(thread);
|
|
401
543
|
return record.entry;
|
|
402
544
|
})().catch(async (error) => {
|
|
403
|
-
// Not kept:
|
|
404
|
-
if (
|
|
545
|
+
// Not kept: the thread's mark is as it was before it.
|
|
546
|
+
if (was === undefined)
|
|
405
547
|
await settle(thread);
|
|
548
|
+
else
|
|
549
|
+
await wait(thread, was).catch(() => { });
|
|
406
550
|
throw error;
|
|
407
551
|
});
|
|
408
552
|
out.message(thread, messageOf(entry));
|
|
@@ -411,12 +555,54 @@ export function createLooper(options) {
|
|
|
411
555
|
route(thread);
|
|
412
556
|
return { messageId: entry.id };
|
|
413
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
|
+
};
|
|
414
596
|
/** Give a message to the work its thread is followed by. */
|
|
415
597
|
const give = async (work, thread, text) => {
|
|
416
|
-
const sent = await using(work, (loop) => loop.send(text, thread));
|
|
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);
|
|
417
603
|
leftAtClose(work);
|
|
418
604
|
// What waited there, its work decided but not begun, went with it.
|
|
419
|
-
if (
|
|
605
|
+
if (marks.has(thread) && !routes.has(thread))
|
|
420
606
|
await settle(thread);
|
|
421
607
|
return sent;
|
|
422
608
|
};
|
|
@@ -453,17 +639,17 @@ export function createLooper(options) {
|
|
|
453
639
|
log.warn("A turn that was cut off didn't go on.", { key, error: String(error) });
|
|
454
640
|
}
|
|
455
641
|
}
|
|
456
|
-
for (const thread of [...
|
|
642
|
+
for (const [thread, again] of [...marks]) {
|
|
457
643
|
// Closed meanwhile: what waits keeps its mark, for whoever opens the store next.
|
|
458
644
|
if (closed)
|
|
459
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;
|
|
460
649
|
try {
|
|
461
|
-
// In the thread's line, so its mark is
|
|
650
|
+
// In the thread's line, so its mark is written apart from a message kept there.
|
|
462
651
|
await inLine(thread, async () => {
|
|
463
|
-
if (routes.has(thread) ||
|
|
464
|
-
return;
|
|
465
|
-
// Only what was still to go on, and once: whatever comes of it, the next start doesn't.
|
|
466
|
-
if ((await store.get({ key: markOf(thread) })).value !== true || closed)
|
|
652
|
+
if (routes.has(thread) || marks.get(thread) !== true || closed)
|
|
467
653
|
return;
|
|
468
654
|
if (going())
|
|
469
655
|
route(thread);
|
|
@@ -495,9 +681,9 @@ export function createLooper(options) {
|
|
|
495
681
|
* Say a message in a thread. It is recorded and said at once. In a thread a piece of work
|
|
496
682
|
* follows it is that work's: with no turn of it running it starts one, which waits for a place
|
|
497
683
|
* if none is free, and a turn running or waiting takes it at its next model request. In a
|
|
498
|
-
* thread nothing follows, the main loop says whose it is first. Refused `busy`
|
|
499
|
-
* turns already wait and it would start one more, or while the looper is
|
|
500
|
-
* `unavailable` once it has closed.
|
|
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.
|
|
501
687
|
*/
|
|
502
688
|
send(thread, text) {
|
|
503
689
|
try {
|
|
@@ -508,7 +694,7 @@ export function createLooper(options) {
|
|
|
508
694
|
}
|
|
509
695
|
const at = cuts;
|
|
510
696
|
// Work known here is asked at once, so a stop that comes after this cuts it too.
|
|
511
|
-
const known = followed.get(thread) ?? (!
|
|
697
|
+
const known = followed.get(thread) ?? (!marks.has(thread) && works.has(thread) ? thread : undefined);
|
|
512
698
|
if (known !== undefined && !lines.get(thread)?.waiting)
|
|
513
699
|
return give(known, thread, text);
|
|
514
700
|
return started(inLine(thread, async () => {
|
|
@@ -519,7 +705,7 @@ export function createLooper(options) {
|
|
|
519
705
|
}));
|
|
520
706
|
},
|
|
521
707
|
/** Stop a piece of work's turn now. A call already running finishes, and its result is kept. */
|
|
522
|
-
cancel: async (work) => works.get(work)?.loop.cancel(),
|
|
708
|
+
cancel: async (work) => void (await works.get(work)?.loop.cancel()),
|
|
523
709
|
/** What a piece of work's loop is doing. */
|
|
524
710
|
state: (work) => works.get(work)?.loop.state() ?? { state: "idle" },
|
|
525
711
|
/** A thread's messages before `before`, oldest first, as many as fit `limit`. */
|
|
@@ -537,12 +723,13 @@ export function createLooper(options) {
|
|
|
537
723
|
await Promise.all([...loops.map((loop) => loop.cancel()), main.cancel()]);
|
|
538
724
|
// A message being recorded as this was asked is recorded first, and a call still running ends.
|
|
539
725
|
await Promise.all(starting);
|
|
540
|
-
await
|
|
726
|
+
await decided;
|
|
541
727
|
await Promise.all(loops.map((loop) => loop.drained()));
|
|
728
|
+
await Promise.all([...works.values()].map(({ writes }) => writes));
|
|
542
729
|
// What was cut off earlier and hasn't gone on doesn't after a stop either: its marks go.
|
|
543
730
|
// Nor does the next start go on with what waits: a thread's next message does.
|
|
544
731
|
await resume(true)
|
|
545
|
-
.then(() => Promise.all([...
|
|
732
|
+
.then(() => Promise.all([...marks].flatMap(([thread, again]) => (again ? [wait(thread, false)] : []))))
|
|
546
733
|
.catch((error) => log.warn("What was cut off couldn't be looked for.", { error: String(error) }));
|
|
547
734
|
},
|
|
548
735
|
/** Take messages again. The turns `stop` cut stay cut. */
|
|
@@ -561,12 +748,13 @@ export function createLooper(options) {
|
|
|
561
748
|
},
|
|
562
749
|
/** Settles when every piece of work's latest turn has written all it will. */
|
|
563
750
|
async settled() {
|
|
564
|
-
await
|
|
751
|
+
await decided;
|
|
565
752
|
await Promise.all([...works.values()].map(({ loop }) => loop.settled()));
|
|
566
753
|
await Promise.all([...works.values()].map(({ writes }) => writes));
|
|
567
754
|
},
|
|
568
755
|
/** Text the model writes while it answers `requestId`, for the reply it becomes. */
|
|
569
756
|
delta(requestId, text) {
|
|
757
|
+
main.delta(requestId, text);
|
|
570
758
|
for (const { loop } of works.values())
|
|
571
759
|
loop.delta(requestId, text);
|
|
572
760
|
},
|