@agentorg/cli 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/node_modules/@agentorg/computer/LICENSE +201 -0
- package/dist/node_modules/@agentorg/computer/NOTICE +2 -0
- package/dist/node_modules/@agentorg/computer/dist/THIRD_PARTY_LICENSES.txt +1321 -0
- package/dist/node_modules/@agentorg/computer/dist/sdk.js +24909 -0
- package/dist/node_modules/@agentorg/computer/dist/types/generated/schema.d.ts +860 -0
- package/dist/node_modules/@agentorg/computer/dist/types/sdk.d.ts +242 -0
- package/dist/node_modules/@agentorg/computer/package.json +12 -0
- package/dist/node_modules/@agentorg/computer-connector/dist/src/index.d.ts +1 -0
- package/dist/node_modules/@agentorg/computer-connector/dist/src/index.js +1 -0
- package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.d.ts +24 -0
- package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.js +482 -0
- package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.d.ts +19 -0
- package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.js +149 -0
- package/dist/node_modules/@agentorg/computer-connector/package.json +33 -0
- package/dist/node_modules/@agentorg/computer-connector/skills/browser/SKILL.md +31 -0
- package/dist/node_modules/@agentorg/computer-connector/skills/computer/SKILL.md +30 -0
- package/dist/node_modules/@agentorg/computer-connector/skills/files/SKILL.md +28 -0
- package/dist/node_modules/@agentorg/computer-connector/skills/terminal/SKILL.md +39 -0
- package/dist/node_modules/@agentorg/computer-connector/skills/web-research/SKILL.md +29 -0
- package/dist/node_modules/@agentorg/hub/dist/src/router.d.ts +4 -2
- package/dist/node_modules/@agentorg/hub/dist/src/router.js +3 -1
- package/dist/node_modules/@agentorg/hub/dist/src/validate.js +16 -2
- package/dist/node_modules/@agentorg/interfaces/dist/src/agent-provider.d.ts +34 -47
- package/dist/node_modules/@agentorg/interfaces/dist/src/agent-provider.js +25 -34
- package/dist/node_modules/@agentorg/interfaces/dist/src/chat.d.ts +90 -50
- package/dist/node_modules/@agentorg/interfaces/dist/src/chat.js +69 -34
- package/dist/node_modules/@agentorg/interfaces/dist/src/connection.d.ts +36 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/connection.js +31 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/folders.d.ts +19 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/folders.js +25 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/index.d.ts +6 -4
- package/dist/node_modules/@agentorg/interfaces/dist/src/index.js +6 -4
- package/dist/node_modules/@agentorg/interfaces/dist/src/llm-session.d.ts +2 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/llm-session.js +2 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/manager.d.ts +381 -27
- package/dist/node_modules/@agentorg/interfaces/dist/src/manager.js +179 -15
- package/dist/node_modules/@agentorg/interfaces/dist/src/units.d.ts +460 -0
- package/dist/node_modules/@agentorg/interfaces/dist/src/units.js +204 -0
- package/dist/node_modules/@agentorg/looper/dist/src/index.d.ts +5 -0
- package/dist/node_modules/@agentorg/looper/dist/src/index.js +5 -0
- package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +89 -0
- package/dist/node_modules/@agentorg/looper/dist/src/loop.js +366 -0
- package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +81 -0
- package/dist/node_modules/@agentorg/looper/dist/src/looper.js +574 -0
- package/dist/node_modules/@agentorg/looper/dist/src/main.d.ts +34 -0
- package/dist/node_modules/@agentorg/looper/dist/src/main.js +116 -0
- package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +181 -0
- package/dist/node_modules/@agentorg/looper/dist/src/record.js +433 -0
- package/dist/node_modules/@agentorg/looper/dist/src/tools.d.ts +11 -0
- package/dist/node_modules/@agentorg/looper/dist/src/tools.js +42 -0
- package/dist/node_modules/@agentorg/looper/dist/src/types.d.ts +147 -0
- package/dist/node_modules/@agentorg/looper/dist/src/types.js +32 -0
- package/dist/node_modules/@agentorg/looper/package.json +24 -0
- package/dist/node_modules/@agentorg/manager/dist/src/activity.d.ts +18 -0
- package/dist/node_modules/@agentorg/manager/dist/src/activity.js +65 -0
- package/dist/node_modules/@agentorg/manager/dist/src/control.d.ts +134 -0
- package/dist/node_modules/@agentorg/manager/dist/src/control.js +1115 -0
- package/dist/node_modules/@agentorg/manager/dist/src/form.d.ts +40 -0
- package/dist/node_modules/@agentorg/manager/dist/src/form.js +191 -0
- package/dist/node_modules/@agentorg/manager/dist/src/manager.d.ts +4 -0
- package/dist/node_modules/@agentorg/manager/dist/src/manager.js +341 -189
- package/dist/node_modules/@agentorg/manager/dist/src/skills.d.ts +2 -0
- package/dist/node_modules/@agentorg/manager/dist/src/skills.js +7 -0
- package/dist/node_modules/@agentorg/manager/dist/src/stack.d.ts +454 -0
- package/dist/node_modules/@agentorg/manager/dist/src/stack.js +917 -0
- package/dist/node_modules/@agentorg/mcp/dist/src/connection.d.ts +2 -1
- package/dist/node_modules/@agentorg/mcp/dist/src/connection.js +8 -2
- package/dist/node_modules/@agentorg/mcp/dist/src/index.d.ts +3 -0
- package/dist/node_modules/@agentorg/mcp/dist/src/index.js +3 -0
- package/dist/node_modules/@agentorg/mcp/dist/src/kinds.d.ts +108 -0
- package/dist/node_modules/@agentorg/mcp/dist/src/kinds.js +95 -0
- package/dist/node_modules/@agentorg/mcp/dist/src/provider.d.ts +2 -2
- package/dist/node_modules/@agentorg/mcp/dist/src/provider.js +48 -18
- package/dist/node_modules/@agentorg/mcp/dist/src/public.d.ts +6 -0
- package/dist/node_modules/@agentorg/mcp/dist/src/public.js +72 -0
- package/dist/node_modules/@agentorg/sdk/dist/src/filled.d.ts +21 -0
- package/dist/node_modules/@agentorg/sdk/dist/src/filled.js +24 -0
- package/dist/node_modules/@agentorg/sdk/dist/src/index.d.ts +1 -0
- package/dist/node_modules/@agentorg/sdk/dist/src/index.js +1 -0
- package/dist/node_modules/@agentorg/shell/dist/app.css +1306 -181
- package/dist/node_modules/@agentorg/shell/dist/app.js +207 -166
- package/dist/node_modules/@agentorg/shell/package.json +6 -1
- package/dist/node_modules/assistant/dist/src/chats.d.ts +94 -0
- package/dist/node_modules/assistant/dist/src/chats.js +355 -0
- package/dist/node_modules/assistant/dist/src/engine.d.ts +26 -7
- package/dist/node_modules/assistant/dist/src/engine.js +107 -35
- package/dist/node_modules/assistant/dist/src/local-store.js +1 -1
- package/dist/node_modules/assistant/dist/src/provider.d.ts +1 -1
- package/dist/node_modules/assistant/dist/src/provider.js +47 -19
- package/dist/node_modules/assistant/dist/src/tools.d.ts +11 -28
- package/dist/node_modules/assistant/dist/src/tools.js +19 -40
- package/dist/node_modules/assistant/package.json +1 -0
- package/dist/src/api.js +4 -0
- package/dist/src/folders.d.ts +2 -0
- package/dist/src/folders.js +45 -0
- package/dist/src/gateway.d.ts +7 -1
- package/dist/src/gateway.js +81 -12
- package/dist/src/live.d.ts +17 -0
- package/dist/src/live.js +109 -0
- package/dist/src/programs.d.ts +7 -0
- package/dist/src/programs.js +29 -2
- package/dist/src/signin.d.ts +12 -3
- package/dist/src/signin.js +30 -6
- package/package.json +2 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/mcp-provider.d.ts +0 -88
- package/dist/node_modules/@agentorg/interfaces/dist/src/mcp-provider.js +0 -78
- package/dist/node_modules/assistant/dist/src/agent.d.ts +0 -62
- package/dist/node_modules/assistant/dist/src/agent.js +0 -226
- package/dist/node_modules/assistant/dist/src/history.d.ts +0 -97
- package/dist/node_modules/assistant/dist/src/history.js +0 -235
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
5
|
+
*
|
|
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 says whose it is: new work, which has the
|
|
8
|
+
* thread's name as its id, or work in a turn now, which then follows that thread too. The main
|
|
9
|
+
* loop makes one decision at a time, and one for a thread on all that waits in it. A piece of
|
|
10
|
+
* work answers in the thread of the newest message it has read.
|
|
11
|
+
*
|
|
12
|
+
* Turns run side by side, each in a place; the main loop takes none. There are `PLACES` of them,
|
|
13
|
+
* and one more that only work already begun (it has done something) may take, so new work never
|
|
14
|
+
* keeps a message from work that is under way. A turn that finds no place waits for one, in the
|
|
15
|
+
* order it came. While begun work keeps five turns running, new work goes on waiting. `WAITING`
|
|
16
|
+
* turns may wait; past that a message that would start a turn is refused `busy`.
|
|
17
|
+
*
|
|
18
|
+
* A piece of work is marked in the store while a turn of it hasn't ended, and a thread while
|
|
19
|
+
* messages wait in it. A looper that closes under either, or goes without closing, leaves the
|
|
20
|
+
* mark, and `resume` on the same store goes on with it, once. A turn that ends, fails or is
|
|
21
|
+
* stopped leaves no mark; messages the main loop couldn't place, or was stopped over, wait for
|
|
22
|
+
* their thread's next message, and all of them are put to the main loop together.
|
|
23
|
+
*/
|
|
24
|
+
import { createLoop } from "./loop.js";
|
|
25
|
+
import { createMain, titled } from "./main.js";
|
|
26
|
+
import { createRecord, createThread, messageOf, READS, textOf, userEntry, } from "./record.js";
|
|
27
|
+
import { codeOf, Refusal, } from "./types.js";
|
|
28
|
+
/** The turns that run at once, not counting the one more for work already begun. */
|
|
29
|
+
export const PLACES = 4;
|
|
30
|
+
/** The turns that may wait for a place at once. */
|
|
31
|
+
export const WAITING = 16;
|
|
32
|
+
/** Where a thread's record is kept in the looper's store. */
|
|
33
|
+
const recordOf = (thread) => `thread/${encodeURIComponent(thread)}/`;
|
|
34
|
+
/** Where a piece of work is marked while a turn of it hasn't ended. */
|
|
35
|
+
const UNFINISHED = "unfinished/";
|
|
36
|
+
/**
|
|
37
|
+
* Where a thread is marked while messages wait in it for a piece of work to take them: `true`
|
|
38
|
+
* while that is still to happen, so the next start goes on with it, and `false` once it couldn't
|
|
39
|
+
* or was stopped, when the thread's next message does.
|
|
40
|
+
*/
|
|
41
|
+
const UNROUTED = "unrouted/";
|
|
42
|
+
/** Where a piece of work's title and the threads it follows are kept. */
|
|
43
|
+
const WORKS = "work/";
|
|
44
|
+
const running = (status) => status.state === "thinking" || status.state === "tool";
|
|
45
|
+
/** The store's keys under `prefix`, as a store of its own. */
|
|
46
|
+
function under(store, prefix) {
|
|
47
|
+
return {
|
|
48
|
+
get: ({ key }) => store.get({ key: prefix + key }),
|
|
49
|
+
set: ({ key, value }) => store.set({ key: prefix + key, value }),
|
|
50
|
+
delete: ({ key }) => store.delete({ key: prefix + key }),
|
|
51
|
+
async list({ prefix: inner = "", after, limit }) {
|
|
52
|
+
const page = await store.list({
|
|
53
|
+
prefix: prefix + inner,
|
|
54
|
+
...(after === undefined ? {} : { after: prefix + after }),
|
|
55
|
+
...(limit === undefined ? {} : { limit }),
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
keys: page.keys.map((key) => key.slice(prefix.length)),
|
|
59
|
+
next: page.next === null ? null : page.next.slice(prefix.length),
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Make a looper. Nothing is read or run until a thread is named: a message sent in it, or its
|
|
66
|
+
* messages asked for.
|
|
67
|
+
*/
|
|
68
|
+
export function createLooper(options) {
|
|
69
|
+
const { store, out, log } = options;
|
|
70
|
+
/** The pieces of work that have a loop here, by id. */
|
|
71
|
+
const works = new Map();
|
|
72
|
+
const threads = new Map();
|
|
73
|
+
/** A thread's messages, read when first asked for. */
|
|
74
|
+
const threadOf = (name) => {
|
|
75
|
+
const found = threads.get(name) ?? createThread(under(store, recordOf(name)), log);
|
|
76
|
+
threads.set(name, found);
|
|
77
|
+
return found;
|
|
78
|
+
};
|
|
79
|
+
/** What is kept of each piece of work the main loop placed, and the work each thread has. */
|
|
80
|
+
const kept = new Map();
|
|
81
|
+
const followed = new Map();
|
|
82
|
+
/** The threads that are marked: messages wait in them. */
|
|
83
|
+
const unrouted = new Set();
|
|
84
|
+
const markOf = (thread) => UNROUTED + encodeURIComponent(thread);
|
|
85
|
+
/** Keep a thread's mark: `again` is whether the next start goes on with what waits there. */
|
|
86
|
+
const wait = (thread, again) => store.set({ key: markOf(thread), value: again });
|
|
87
|
+
/** Take a thread's mark off: nothing waits there. */
|
|
88
|
+
const settle = async (thread) => {
|
|
89
|
+
unrouted.delete(thread);
|
|
90
|
+
await store
|
|
91
|
+
.delete({ key: markOf(thread) })
|
|
92
|
+
.catch((error) => log.warn("A thread's mark couldn't be taken off.", { thread, error: String(error) }));
|
|
93
|
+
};
|
|
94
|
+
/** The threads being decided or given now; each settles when that has ended. */
|
|
95
|
+
const routes = new Map();
|
|
96
|
+
/** Set while it is stopped: no turn starts until it is started. */
|
|
97
|
+
let stopped = options.stopped ?? false;
|
|
98
|
+
/** Set when it closes: no turn starts after. */
|
|
99
|
+
let closed = false;
|
|
100
|
+
/** Counts the stops and the close: what was asked before one starts nothing after it. */
|
|
101
|
+
let cuts = 0;
|
|
102
|
+
/** Refuse a turn that can't start now, or was asked for before a stop, whatever its thread. */
|
|
103
|
+
const mayStart = (at = cuts) => {
|
|
104
|
+
if (closed)
|
|
105
|
+
throw new Refusal("unavailable", "This agent is closing.");
|
|
106
|
+
if (stopped || at !== cuts)
|
|
107
|
+
throw new Refusal("busy", "It is stopped. Start it to go on.");
|
|
108
|
+
};
|
|
109
|
+
/** Messages being recorded now, each about to start a turn: stopping waits for them. */
|
|
110
|
+
const starting = new Set();
|
|
111
|
+
const started = (call) => {
|
|
112
|
+
const done = call.then(() => void starting.delete(done), () => void starting.delete(done));
|
|
113
|
+
starting.add(done);
|
|
114
|
+
return call;
|
|
115
|
+
};
|
|
116
|
+
/** Whether anything is at work, as it was last said. */
|
|
117
|
+
let working = false;
|
|
118
|
+
const tally = () => {
|
|
119
|
+
const now = routes.size > 0 || [...works.values()].some(({ loop }) => running(loop.state()));
|
|
120
|
+
if (now === working)
|
|
121
|
+
return;
|
|
122
|
+
working = now;
|
|
123
|
+
out.working(now);
|
|
124
|
+
};
|
|
125
|
+
/** The turns that have a place, and those waiting for one, first come first. */
|
|
126
|
+
const placed = new Set();
|
|
127
|
+
const waiting = [];
|
|
128
|
+
const fits = (begun) => placed.size < PLACES + (begun ? 1 : 0);
|
|
129
|
+
const enter = (begun) => {
|
|
130
|
+
let take = () => { };
|
|
131
|
+
const place = {
|
|
132
|
+
entered: new Promise((resolve) => {
|
|
133
|
+
take = resolve;
|
|
134
|
+
}),
|
|
135
|
+
leave() {
|
|
136
|
+
take();
|
|
137
|
+
const queued = waiting.findIndex((one) => one.place === place);
|
|
138
|
+
if (queued !== -1)
|
|
139
|
+
waiting.splice(queued, 1);
|
|
140
|
+
if (!placed.delete(place))
|
|
141
|
+
return;
|
|
142
|
+
// Its place goes to whoever has waited longest for one they may take.
|
|
143
|
+
const next = waiting.find((one) => fits(one.begun));
|
|
144
|
+
if (!next)
|
|
145
|
+
return;
|
|
146
|
+
waiting.splice(waiting.indexOf(next), 1);
|
|
147
|
+
placed.add(next.place);
|
|
148
|
+
next.take();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
if (fits(begun)) {
|
|
152
|
+
placed.add(place);
|
|
153
|
+
take();
|
|
154
|
+
}
|
|
155
|
+
else if (waiting.length < WAITING)
|
|
156
|
+
waiting.push({ place, begun, take });
|
|
157
|
+
else
|
|
158
|
+
throw new Refusal("busy", "Too many turns are waiting. Wait for one to start.");
|
|
159
|
+
return place;
|
|
160
|
+
};
|
|
161
|
+
/** What the store holds of the work the main loop placed, and of what waits: read once. */
|
|
162
|
+
let learned;
|
|
163
|
+
const keysUnder = async (prefix) => {
|
|
164
|
+
const keys = [];
|
|
165
|
+
let after;
|
|
166
|
+
do {
|
|
167
|
+
const page = await store.list({ prefix, ...(after ? { after } : {}) });
|
|
168
|
+
keys.push(...page.keys);
|
|
169
|
+
after = page.next ?? undefined;
|
|
170
|
+
} while (after);
|
|
171
|
+
return keys;
|
|
172
|
+
};
|
|
173
|
+
const learn = () => {
|
|
174
|
+
if (learned)
|
|
175
|
+
return learned;
|
|
176
|
+
const attempt = (async () => {
|
|
177
|
+
const keys = await keysUnder(WORKS);
|
|
178
|
+
for (let at = 0; at < keys.length; at += READS) {
|
|
179
|
+
const some = keys.slice(at, at + READS);
|
|
180
|
+
const values = await Promise.all(some.map((key) => store.get({ key })));
|
|
181
|
+
some.forEach((key, i) => {
|
|
182
|
+
const value = values[i]?.value;
|
|
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
|
+
});
|
|
195
|
+
}
|
|
196
|
+
for (const key of await keysUnder(UNROUTED))
|
|
197
|
+
unrouted.add(decodeURIComponent(key.slice(UNROUTED.length)));
|
|
198
|
+
})();
|
|
199
|
+
learned = attempt;
|
|
200
|
+
attempt.catch(() => {
|
|
201
|
+
if (learned === attempt)
|
|
202
|
+
learned = undefined;
|
|
203
|
+
});
|
|
204
|
+
return attempt;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* 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 none of them waits.
|
|
209
|
+
*/
|
|
210
|
+
const workOf = async (thread) => {
|
|
211
|
+
await learn();
|
|
212
|
+
const work = followed.get(thread);
|
|
213
|
+
if (work !== undefined)
|
|
214
|
+
return work;
|
|
215
|
+
if (unrouted.has(thread))
|
|
216
|
+
return undefined;
|
|
217
|
+
return (await threadOf(thread).load()).records.length > 0 ? thread : undefined;
|
|
218
|
+
};
|
|
219
|
+
const open = (id) => {
|
|
220
|
+
const record = createRecord({
|
|
221
|
+
id,
|
|
222
|
+
follows: () => kept.get(id)?.follows ?? [id],
|
|
223
|
+
thread: threadOf,
|
|
224
|
+
});
|
|
225
|
+
const key = UNFINISHED + encodeURIComponent(id);
|
|
226
|
+
const held = {
|
|
227
|
+
record,
|
|
228
|
+
/** Whether a turn of it is running, as its mark was last asked to say. */
|
|
229
|
+
marked: false,
|
|
230
|
+
/** Its mark's writes, one after another. */
|
|
231
|
+
writes: Promise.resolve(),
|
|
232
|
+
/** Keep its mark, or take it off, after any write before. */
|
|
233
|
+
mark(on) {
|
|
234
|
+
held.writes = held.writes
|
|
235
|
+
.then(() => (on ? store.set({ key, value: true }) : store.delete({ key })))
|
|
236
|
+
.then(() => { }, (error) => log.error("A work's mark couldn't be written.", { work: id, error: String(error) }));
|
|
237
|
+
},
|
|
238
|
+
loop: createLoop({
|
|
239
|
+
request: options.request,
|
|
240
|
+
model: options.model,
|
|
241
|
+
enter,
|
|
242
|
+
record,
|
|
243
|
+
out: {
|
|
244
|
+
message: out.message,
|
|
245
|
+
delta: out.delta,
|
|
246
|
+
failed: out.failed,
|
|
247
|
+
status(status) {
|
|
248
|
+
const on = running(status);
|
|
249
|
+
// A turn that starts is marked, but one that goes on from a cut-off has that mark
|
|
250
|
+
// taken off: it goes on once. A turn that ends takes its mark off, but one cut by the
|
|
251
|
+
// looper closing keeps it, for whoever opens the store next.
|
|
252
|
+
if (on && !held.marked)
|
|
253
|
+
held.mark(!held.loop.resumed());
|
|
254
|
+
else if (!on && held.marked && !closed)
|
|
255
|
+
held.mark(false);
|
|
256
|
+
held.marked = on;
|
|
257
|
+
tally();
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
log,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
return held;
|
|
264
|
+
};
|
|
265
|
+
const of = (id) => {
|
|
266
|
+
const found = works.get(id) ?? open(id);
|
|
267
|
+
works.set(id, found);
|
|
268
|
+
return found;
|
|
269
|
+
};
|
|
270
|
+
/** A work's title: the main loop's, or its opening line for work begun before it had one. */
|
|
271
|
+
const titleOf = async (id) => kept.get(id)?.title || titled(await of(id).record.opening());
|
|
272
|
+
/** Ask something of a piece of work's loop. */
|
|
273
|
+
const using = (id, ask) => started(ask(of(id).loop));
|
|
274
|
+
/** Work given something as the looper closed, its turn cut before it began: marked like one. */
|
|
275
|
+
const leftAtClose = (id) => {
|
|
276
|
+
const work = works.get(id);
|
|
277
|
+
if (closed && work && !work.marked)
|
|
278
|
+
work.mark(true);
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* What is asked of a thread whose work has to be found, or is being given it, taken one at a
|
|
282
|
+
* time in the order it was asked; and how much is still to be taken.
|
|
283
|
+
*/
|
|
284
|
+
const lines = new Map();
|
|
285
|
+
const inLine = (thread, take) => {
|
|
286
|
+
const line = lines.get(thread) ?? { last: Promise.resolve(), waiting: 0 };
|
|
287
|
+
lines.set(thread, line);
|
|
288
|
+
line.waiting++;
|
|
289
|
+
const taken = line.last.then(take).finally(() => void line.waiting--);
|
|
290
|
+
line.last = taken.catch(() => { });
|
|
291
|
+
return taken;
|
|
292
|
+
};
|
|
293
|
+
const main = createMain({
|
|
294
|
+
model: options.model,
|
|
295
|
+
chosen: async () => (await options.request()).model,
|
|
296
|
+
log,
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* Have the main loop say whose the messages waiting in a thread are, and keep what it says:
|
|
300
|
+
* new work of the thread's own, or work in a turn now, which then follows the thread too.
|
|
301
|
+
* Nothing is decided, and it says so with none, once `cut` says the looper stopped or closed.
|
|
302
|
+
*/
|
|
303
|
+
const decide = async (thread, cut) => {
|
|
304
|
+
// Read in the thread's line: a message being kept there now is kept first.
|
|
305
|
+
const texts = await inLine(thread, async () => {
|
|
306
|
+
const { records } = await threadOf(thread).load();
|
|
307
|
+
const said = records.flatMap(({ entry }) => (entry.role === "user" ? [textOf(entry)] : []));
|
|
308
|
+
// Marked before a message that was never kept: nothing waits.
|
|
309
|
+
if (said.length === 0)
|
|
310
|
+
await settle(thread);
|
|
311
|
+
return said;
|
|
312
|
+
});
|
|
313
|
+
if (texts.length === 0)
|
|
314
|
+
return undefined;
|
|
315
|
+
const underway = await Promise.all([...works]
|
|
316
|
+
.filter(([, { loop }]) => running(loop.state()))
|
|
317
|
+
.map(async ([id]) => ({ id, title: await titleOf(id) })));
|
|
318
|
+
if (cut())
|
|
319
|
+
return undefined;
|
|
320
|
+
const routed = await main.route(texts, underway);
|
|
321
|
+
if (!routed || cut())
|
|
322
|
+
return undefined;
|
|
323
|
+
const work = "work" in routed ? routed.work : thread;
|
|
324
|
+
const record = {
|
|
325
|
+
title: "work" in routed ? await titleOf(work) : routed.title,
|
|
326
|
+
follows: [...new Set([...(kept.get(work)?.follows ?? [work]), thread])],
|
|
327
|
+
};
|
|
328
|
+
// Kept before the messages are taken, so a start in between finds whose they are.
|
|
329
|
+
await store.set({ key: WORKS + encodeURIComponent(work), value: record });
|
|
330
|
+
kept.set(work, record);
|
|
331
|
+
followed.set(thread, work);
|
|
332
|
+
return work;
|
|
333
|
+
};
|
|
334
|
+
/** Decisions are made one at a time, each to its end, so each knows what the last began. */
|
|
335
|
+
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
|
+
/**
|
|
339
|
+
* Give the messages waiting in a thread to a piece of work: the one it has, or the one the main
|
|
340
|
+
* 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 asked about when that has ended without taking it. Once the looper has
|
|
342
|
+
* stopped or closed since this was asked, nothing starts, and what waits goes on waiting.
|
|
343
|
+
*/
|
|
344
|
+
const route = (thread) => {
|
|
345
|
+
if (stopped || closed)
|
|
346
|
+
return;
|
|
347
|
+
if (routes.has(thread))
|
|
348
|
+
return void again.add(thread);
|
|
349
|
+
const at = cuts;
|
|
350
|
+
const cut = () => at !== cuts;
|
|
351
|
+
const routing = decided
|
|
352
|
+
.then(async () => {
|
|
353
|
+
if (cut())
|
|
354
|
+
return;
|
|
355
|
+
const work = followed.get(thread) ?? (await decide(thread, cut));
|
|
356
|
+
if (work === undefined)
|
|
357
|
+
return;
|
|
358
|
+
// In the thread's line: a message being kept there now is kept first, and goes with them.
|
|
359
|
+
await inLine(thread, async () => {
|
|
360
|
+
if (cut())
|
|
361
|
+
return;
|
|
362
|
+
if (!(await using(work, (loop) => loop.adopt(thread)))) {
|
|
363
|
+
// Its work was told to stop first: they wait for the thread's next message.
|
|
364
|
+
if (!cut())
|
|
365
|
+
await wait(thread, false);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
leftAtClose(work);
|
|
369
|
+
await settle(thread);
|
|
370
|
+
});
|
|
371
|
+
})
|
|
372
|
+
.catch(async (error) => {
|
|
373
|
+
if (closed)
|
|
374
|
+
return;
|
|
375
|
+
log.error("A message got no answer.", { thread, error: String(error) });
|
|
376
|
+
out.failed(thread, codeOf(error));
|
|
377
|
+
// Still waiting: for the thread's next message, not for the next start.
|
|
378
|
+
if (unrouted.has(thread))
|
|
379
|
+
await wait(thread, false).catch(() => { });
|
|
380
|
+
})
|
|
381
|
+
.finally(() => {
|
|
382
|
+
routes.delete(thread);
|
|
383
|
+
// Not once it has stopped or closed since: then what waits goes on waiting.
|
|
384
|
+
if (again.delete(thread) && !cut() && unrouted.has(thread))
|
|
385
|
+
route(thread);
|
|
386
|
+
tally();
|
|
387
|
+
});
|
|
388
|
+
decided = routing;
|
|
389
|
+
routes.set(thread, routing);
|
|
390
|
+
tally();
|
|
391
|
+
};
|
|
392
|
+
/** Keep a message in a thread nothing follows, and have it routed, unless a stop came since `at`. */
|
|
393
|
+
const intake = async (thread, text, at) => {
|
|
394
|
+
const waited = unrouted.has(thread);
|
|
395
|
+
unrouted.add(thread);
|
|
396
|
+
const entry = await (async () => {
|
|
397
|
+
await wait(thread, true);
|
|
398
|
+
const into = threadOf(thread);
|
|
399
|
+
const { record, stored } = into.push(await into.load(), userEntry(text));
|
|
400
|
+
await stored;
|
|
401
|
+
return record.entry;
|
|
402
|
+
})().catch(async (error) => {
|
|
403
|
+
// Not kept: nothing of it waits, unless an earlier message of the thread does.
|
|
404
|
+
if (!waited)
|
|
405
|
+
await settle(thread);
|
|
406
|
+
throw error;
|
|
407
|
+
});
|
|
408
|
+
out.message(thread, messageOf(entry));
|
|
409
|
+
// Stopped or closed while it was kept, it waits: for the next message, or the next start.
|
|
410
|
+
if (at === cuts)
|
|
411
|
+
route(thread);
|
|
412
|
+
return { messageId: entry.id };
|
|
413
|
+
};
|
|
414
|
+
/** Give a message to the work its thread is followed by. */
|
|
415
|
+
const give = async (work, thread, text) => {
|
|
416
|
+
const sent = await using(work, (loop) => loop.send(text, thread));
|
|
417
|
+
leftAtClose(work);
|
|
418
|
+
// What waited there, its work decided but not begun, went with it.
|
|
419
|
+
if (unrouted.has(thread) && !routes.has(thread))
|
|
420
|
+
await settle(thread);
|
|
421
|
+
return sent;
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* Go on with what was cut off when this store was last used, by a looper closing under it or
|
|
425
|
+
* going without closing: each turn that hadn't ended, once, waiting for a place like any turn;
|
|
426
|
+
* and each thread whose messages were still to be given to a piece of work, once too. A turn
|
|
427
|
+
* that can't go on now keeps its mark for the next time. A stopped looper goes on with none:
|
|
428
|
+
* its turns' marks go, and its waiting messages wait for their thread's next. Whoever hosts the
|
|
429
|
+
* looper calls this when a request can be made; once the marks have been read, a later call
|
|
430
|
+
* does nothing. `drop` goes on with nothing, as a stopped looper does.
|
|
431
|
+
*/
|
|
432
|
+
const goOn = async (drop) => {
|
|
433
|
+
const at = cuts;
|
|
434
|
+
/** Whether anything may still go on: not once it has stopped or closed since this began. */
|
|
435
|
+
const going = () => !drop && !stopped && at === cuts;
|
|
436
|
+
await learn();
|
|
437
|
+
const keys = await keysUnder(UNFINISHED);
|
|
438
|
+
for (const key of keys) {
|
|
439
|
+
// Closed meanwhile: what is left stays marked, for whoever opens the store next.
|
|
440
|
+
if (closed)
|
|
441
|
+
return;
|
|
442
|
+
const id = decodeURIComponent(key.slice(UNFINISHED.length));
|
|
443
|
+
const held = of(id);
|
|
444
|
+
try {
|
|
445
|
+
if (going())
|
|
446
|
+
await using(id, (loop) => loop.resume());
|
|
447
|
+
// Work in a turn now keeps the mark as that turn's, and a looper that has closed leaves
|
|
448
|
+
// it. Otherwise nothing goes on, and it goes.
|
|
449
|
+
if (!held.marked && !closed)
|
|
450
|
+
held.mark(false);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
log.warn("A turn that was cut off didn't go on.", { key, error: String(error) });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const thread of [...unrouted]) {
|
|
457
|
+
// Closed meanwhile: what waits keeps its mark, for whoever opens the store next.
|
|
458
|
+
if (closed)
|
|
459
|
+
return;
|
|
460
|
+
try {
|
|
461
|
+
// In the thread's line, so its mark is read and written apart from a message kept there.
|
|
462
|
+
await inLine(thread, async () => {
|
|
463
|
+
if (routes.has(thread) || !unrouted.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)
|
|
467
|
+
return;
|
|
468
|
+
if (going())
|
|
469
|
+
route(thread);
|
|
470
|
+
await wait(thread, false);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
log.warn("A message that was waiting wasn't placed.", { thread, error: String(error) });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
/** What was cut off is looked for once; a look that couldn't read the store is made again. */
|
|
479
|
+
let looked;
|
|
480
|
+
const resume = (drop = false) => {
|
|
481
|
+
if (closed)
|
|
482
|
+
return Promise.resolve();
|
|
483
|
+
if (!looked) {
|
|
484
|
+
const look = goOn(drop);
|
|
485
|
+
looked = look;
|
|
486
|
+
look.catch(() => {
|
|
487
|
+
if (looked === look)
|
|
488
|
+
looked = undefined;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return looked;
|
|
492
|
+
};
|
|
493
|
+
return {
|
|
494
|
+
/**
|
|
495
|
+
* Say a message in a thread. It is recorded and said at once. In a thread a piece of work
|
|
496
|
+
* follows it is that work's: with no turn of it running it starts one, which waits for a place
|
|
497
|
+
* 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` while `WAITING`
|
|
499
|
+
* turns already wait and it would start one more, or while the looper is stopped; and
|
|
500
|
+
* `unavailable` once it has closed.
|
|
501
|
+
*/
|
|
502
|
+
send(thread, text) {
|
|
503
|
+
try {
|
|
504
|
+
mayStart();
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
return Promise.reject(error);
|
|
508
|
+
}
|
|
509
|
+
const at = cuts;
|
|
510
|
+
// Work known here is asked at once, so a stop that comes after this cuts it too.
|
|
511
|
+
const known = followed.get(thread) ?? (!unrouted.has(thread) && works.has(thread) ? thread : undefined);
|
|
512
|
+
if (known !== undefined && !lines.get(thread)?.waiting)
|
|
513
|
+
return give(known, thread, text);
|
|
514
|
+
return started(inLine(thread, async () => {
|
|
515
|
+
const work = await workOf(thread);
|
|
516
|
+
// Stopped or closed while its work was found: nothing of it is kept.
|
|
517
|
+
mayStart(at);
|
|
518
|
+
return work === undefined ? intake(thread, text, at) : give(work, thread, text);
|
|
519
|
+
}));
|
|
520
|
+
},
|
|
521
|
+
/** 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(),
|
|
523
|
+
/** What a piece of work's loop is doing. */
|
|
524
|
+
state: (work) => works.get(work)?.loop.state() ?? { state: "idle" },
|
|
525
|
+
/** A thread's messages before `before`, oldest first, as many as fit `limit`. */
|
|
526
|
+
page: (thread, before, limit) => threadOf(thread).page(before, limit),
|
|
527
|
+
/**
|
|
528
|
+
* Stop: every turn is cut, running or waiting, the main loop is cut too, and nothing starts
|
|
529
|
+
* until `start`. A call already running finishes first, so this answers when nothing more will
|
|
530
|
+
* be written. The records stay as they are, to be read. What was cut off earlier no longer
|
|
531
|
+
* goes on, and messages waiting for a piece of work wait for their thread's next.
|
|
532
|
+
*/
|
|
533
|
+
async stop() {
|
|
534
|
+
stopped = true;
|
|
535
|
+
cuts++;
|
|
536
|
+
const loops = [...works.values()].map(({ loop }) => loop);
|
|
537
|
+
await Promise.all([...loops.map((loop) => loop.cancel()), main.cancel()]);
|
|
538
|
+
// A message being recorded as this was asked is recorded first, and a call still running ends.
|
|
539
|
+
await Promise.all(starting);
|
|
540
|
+
await Promise.all(routes.values());
|
|
541
|
+
await Promise.all(loops.map((loop) => loop.drained()));
|
|
542
|
+
// What was cut off earlier and hasn't gone on doesn't after a stop either: its marks go.
|
|
543
|
+
// Nor does the next start go on with what waits: a thread's next message does.
|
|
544
|
+
await resume(true)
|
|
545
|
+
.then(() => Promise.all([...unrouted].map((thread) => wait(thread, false))))
|
|
546
|
+
.catch((error) => log.warn("What was cut off couldn't be looked for.", { error: String(error) }));
|
|
547
|
+
},
|
|
548
|
+
/** Take messages again. The turns `stop` cut stay cut. */
|
|
549
|
+
start() {
|
|
550
|
+
stopped = false;
|
|
551
|
+
},
|
|
552
|
+
resume: () => resume(),
|
|
553
|
+
/**
|
|
554
|
+
* Close for good: every turn is cut, and none starts after. Each stays marked, as does each
|
|
555
|
+
* thread where messages wait, for `resume` in whoever opens the store next.
|
|
556
|
+
*/
|
|
557
|
+
async close() {
|
|
558
|
+
closed = true;
|
|
559
|
+
cuts++;
|
|
560
|
+
await Promise.all([...[...works.values()].map(({ loop }) => loop.cancel()), main.cancel()]);
|
|
561
|
+
},
|
|
562
|
+
/** Settles when every piece of work's latest turn has written all it will. */
|
|
563
|
+
async settled() {
|
|
564
|
+
await Promise.all(routes.values());
|
|
565
|
+
await Promise.all([...works.values()].map(({ loop }) => loop.settled()));
|
|
566
|
+
await Promise.all([...works.values()].map(({ writes }) => writes));
|
|
567
|
+
},
|
|
568
|
+
/** Text the model writes while it answers `requestId`, for the reply it becomes. */
|
|
569
|
+
delta(requestId, text) {
|
|
570
|
+
for (const { loop } of works.values())
|
|
571
|
+
loop.delta(requestId, text);
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|
|
@@ -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
|
+
};
|