@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,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a loop runs on, kept in a store. A thread keeps its messages, one key each: the items a
|
|
3
|
+
* model reads for it, and for a tool message the line a person sees. A thread is read once, then
|
|
4
|
+
* kept in memory, and its oldest messages are deleted past a retention cap. A piece of work reads
|
|
5
|
+
* its context from the threads it follows: each message is kept once, in the thread where it is
|
|
6
|
+
* shown, and says which work it belongs to when that isn't its thread's own.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { Item } from "./types.js";
|
|
10
|
+
/** A message as the record keeps it. */
|
|
11
|
+
export const Entry = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
role: z.enum(["user", "assistant", "tool"]),
|
|
14
|
+
/** What the person sees, for a tool message; the others show their message items. */
|
|
15
|
+
text: z.string().optional(),
|
|
16
|
+
at: z.string(),
|
|
17
|
+
items: z.array(Item),
|
|
18
|
+
/** For a tool message whose call ran: how it ended. */
|
|
19
|
+
state: z.enum(["done", "refused"]).optional(),
|
|
20
|
+
/** For a user message said while a turn was running: it is part of that turn. */
|
|
21
|
+
joined: z.literal(true).optional(),
|
|
22
|
+
/**
|
|
23
|
+
* For what came of a model request that had read its work up to this place, when a user message
|
|
24
|
+
* was said after that: the model reads it ahead of that message, which it doesn't answer.
|
|
25
|
+
*/
|
|
26
|
+
ahead: z.number().optional(),
|
|
27
|
+
/** The piece of work it belongs to, when that isn't its thread's own. */
|
|
28
|
+
work: z.string().optional(),
|
|
29
|
+
/**
|
|
30
|
+
* Its place in its work's order, once that work follows more than one thread. Without it, its
|
|
31
|
+
* place is its number in its thread.
|
|
32
|
+
*/
|
|
33
|
+
place: z.number().optional(),
|
|
34
|
+
});
|
|
35
|
+
/** Where a record's messages are kept in its store. */
|
|
36
|
+
export const MESSAGES = "msg/";
|
|
37
|
+
const keyOf = (seq) => `${MESSAGES}${String(seq).padStart(12, "0")}`;
|
|
38
|
+
const seqOf = (key) => Number(key.slice(MESSAGES.length)) || 0;
|
|
39
|
+
// The record's limits are fixed here for now, sized for a host that carries a model request, or a
|
|
40
|
+
// page of messages, in one piece of about a megabyte.
|
|
41
|
+
/** What the model is sent of the past: the newest exchanges up to this many bytes of JSON. */
|
|
42
|
+
export const INPUT_BUDGET = 400_000;
|
|
43
|
+
/** At most this many messages are kept, in at most this many bytes, oldest deleted first. */
|
|
44
|
+
export const KEEP = 2000;
|
|
45
|
+
export const KEEP_BYTES = 16 * 1024 * 1024;
|
|
46
|
+
/** A stored message's text, and a call's arguments, are cut to fit these, in bytes of JSON. */
|
|
47
|
+
const MAX_TEXT = 192 * 1024;
|
|
48
|
+
const MAX_ARGUMENTS = 16 * 1024;
|
|
49
|
+
/** A page of messages stays under this, so whatever carries it can do so in one piece. */
|
|
50
|
+
const PAGE_BUDGET = 512 * 1024;
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
const size = (value) => encoder.encode(JSON.stringify(value)).byteLength;
|
|
53
|
+
const LEFT_OUT = "(Left out: the conversation was too large to send it.)";
|
|
54
|
+
const ARGUMENTS_LEFT_OUT = '{"left_out":"too large to keep"}';
|
|
55
|
+
/** The first `length` UTF-16 units of `text`, never ending inside a surrogate pair. */
|
|
56
|
+
export function head(text, length) {
|
|
57
|
+
const cut = text.slice(0, length);
|
|
58
|
+
return /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
59
|
+
}
|
|
60
|
+
/** `text`, cut so its JSON fits `max` bytes, saying so when it is cut. */
|
|
61
|
+
export function fitted(text, max) {
|
|
62
|
+
let kept = text;
|
|
63
|
+
for (let bytes = size(kept); bytes > max; bytes = size(kept))
|
|
64
|
+
kept = `${head(kept, Math.floor((kept.length * max) / bytes) - 16)}… (cut)`;
|
|
65
|
+
return kept;
|
|
66
|
+
}
|
|
67
|
+
/** A message shows at most this much; the record keeps all of it. */
|
|
68
|
+
const MAX_SHOWN = 100_000;
|
|
69
|
+
/** What the person sees of a message. */
|
|
70
|
+
export const textOf = (entry) => head(entry.text ??
|
|
71
|
+
entry.items.flatMap((item) => (item.type === "message" ? [item.content] : [])).join(""), MAX_SHOWN);
|
|
72
|
+
/** A call's arguments as they are kept and shown: past their cap, a note in their place. */
|
|
73
|
+
export const argumentsOf = (args) => size(args) > MAX_ARGUMENTS ? ARGUMENTS_LEFT_OUT : args;
|
|
74
|
+
/**
|
|
75
|
+
* A message as it is shown. A tool message whose call ran carries `tool`: the call's name, how it
|
|
76
|
+
* ended, its arguments and its output.
|
|
77
|
+
*/
|
|
78
|
+
export function messageOf(entry) {
|
|
79
|
+
const message = { id: entry.id, role: entry.role, text: textOf(entry), at: entry.at };
|
|
80
|
+
const call = entry.items.find((item) => item.type === "function_call");
|
|
81
|
+
if (entry.role !== "tool" || !entry.state || !call)
|
|
82
|
+
return message;
|
|
83
|
+
const output = entry.items.find((item) => item.type === "function_call_output");
|
|
84
|
+
return {
|
|
85
|
+
...message,
|
|
86
|
+
tool: {
|
|
87
|
+
name: head(call.name, 128),
|
|
88
|
+
state: entry.state,
|
|
89
|
+
input: head(call.arguments, MAX_SHOWN),
|
|
90
|
+
...(output ? { output: head(output.output, MAX_SHOWN) } : {}),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** A message as it is stored: each text once, and nothing past its cap. */
|
|
95
|
+
const kept = (entry) => ({
|
|
96
|
+
...entry,
|
|
97
|
+
items: entry.items.map((item) => {
|
|
98
|
+
if (item.type === "message")
|
|
99
|
+
return { ...item, content: fitted(item.content, MAX_TEXT) };
|
|
100
|
+
if (item.type === "function_call_output")
|
|
101
|
+
return { ...item, output: fitted(item.output, MAX_TEXT) };
|
|
102
|
+
return { ...item, arguments: argumentsOf(item.arguments) };
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
/** Stored values read at once when what is kept is first loaded. */
|
|
106
|
+
export const READS = 16;
|
|
107
|
+
/** A user message as it is kept when said. */
|
|
108
|
+
export const userEntry = (text) => ({
|
|
109
|
+
role: "user",
|
|
110
|
+
items: [{ type: "message", role: "user", content: text }],
|
|
111
|
+
});
|
|
112
|
+
/** A thread's messages in a store, by their numbers. */
|
|
113
|
+
export function createThread(store, log, limits = { keep: KEEP, keepBytes: KEEP_BYTES }) {
|
|
114
|
+
let loaded;
|
|
115
|
+
const load = () => {
|
|
116
|
+
if (loaded)
|
|
117
|
+
return loaded;
|
|
118
|
+
const attempt = (async () => {
|
|
119
|
+
const records = [];
|
|
120
|
+
let last = 0;
|
|
121
|
+
let bytes = 0;
|
|
122
|
+
let after;
|
|
123
|
+
do {
|
|
124
|
+
const page = await store.list({
|
|
125
|
+
prefix: MESSAGES,
|
|
126
|
+
limit: 1000,
|
|
127
|
+
...(after ? { after } : {}),
|
|
128
|
+
});
|
|
129
|
+
// A few at a time, in order: a long record shouldn't hold up the first send one read at
|
|
130
|
+
// a time, nor flood its store.
|
|
131
|
+
for (let at = 0; at < page.keys.length; at += READS) {
|
|
132
|
+
const keys = page.keys.slice(at, at + READS);
|
|
133
|
+
const values = await Promise.all(keys.map((key) => store.get({ key })));
|
|
134
|
+
keys.forEach((key, i) => {
|
|
135
|
+
last = Math.max(last, seqOf(key));
|
|
136
|
+
const parsed = Entry.safeParse(values[i]?.value);
|
|
137
|
+
if (!parsed.success) {
|
|
138
|
+
log.warn("A stored message didn't read back; it is left out.", { key });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const record = { seq: seqOf(key), key, entry: parsed.data, bytes: size(parsed.data) };
|
|
142
|
+
records.push(record);
|
|
143
|
+
bytes += record.bytes;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
after = page.next ?? undefined;
|
|
147
|
+
} while (after);
|
|
148
|
+
return { records, last, bytes };
|
|
149
|
+
})();
|
|
150
|
+
loaded = attempt;
|
|
151
|
+
attempt.catch(() => {
|
|
152
|
+
if (loaded === attempt)
|
|
153
|
+
loaded = undefined;
|
|
154
|
+
});
|
|
155
|
+
return attempt;
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
/** What it holds, read once. */
|
|
159
|
+
load,
|
|
160
|
+
/**
|
|
161
|
+
* Keep a message in a thread already read, at once, under `id` or its own number; `stored`
|
|
162
|
+
* settles when it is written and the oldest past the cap are deleted. A tool message the store
|
|
163
|
+
* refuses is still kept in memory, since its call has run; any other is taken out again.
|
|
164
|
+
*/
|
|
165
|
+
push(held, entry, id) {
|
|
166
|
+
const seq = ++held.last;
|
|
167
|
+
const key = keyOf(seq);
|
|
168
|
+
const record = {
|
|
169
|
+
seq,
|
|
170
|
+
key,
|
|
171
|
+
entry: kept({
|
|
172
|
+
id: id ?? key.slice(MESSAGES.length),
|
|
173
|
+
at: new Date().toISOString(),
|
|
174
|
+
...entry,
|
|
175
|
+
}),
|
|
176
|
+
bytes: 0,
|
|
177
|
+
writing: entry.role === "user",
|
|
178
|
+
};
|
|
179
|
+
record.bytes = size(record.entry);
|
|
180
|
+
held.records.push(record);
|
|
181
|
+
held.bytes += record.bytes;
|
|
182
|
+
const stored = (async () => {
|
|
183
|
+
try {
|
|
184
|
+
await store.set({ key, value: record.entry });
|
|
185
|
+
record.writing = false;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (entry.role !== "tool") {
|
|
189
|
+
held.records.splice(held.records.indexOf(record), 1);
|
|
190
|
+
held.bytes -= record.bytes;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
log.error("A tool message couldn't be stored; it is kept until a restart.", {
|
|
194
|
+
error: String(error),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
while (held.records.length > 1 &&
|
|
198
|
+
(held.records.length > limits.keep || held.bytes > limits.keepBytes)) {
|
|
199
|
+
const oldest = held.records.shift();
|
|
200
|
+
held.bytes -= oldest.bytes;
|
|
201
|
+
await store.delete({ key: oldest.key }).catch((error) => log.warn("An old message couldn't be deleted.", {
|
|
202
|
+
key: oldest.key,
|
|
203
|
+
error: String(error),
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
207
|
+
return { record, stored };
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* Keep a message as `entry` from now on: at once in memory, then in the store. A write the
|
|
211
|
+
* store refuses leaves it as it was.
|
|
212
|
+
*/
|
|
213
|
+
async rewrite(held, record, entry) {
|
|
214
|
+
const was = { entry: record.entry, bytes: record.bytes };
|
|
215
|
+
const swap = (to) => {
|
|
216
|
+
held.bytes += to.bytes - record.bytes;
|
|
217
|
+
Object.assign(record, to);
|
|
218
|
+
};
|
|
219
|
+
swap({ entry, bytes: size(entry) });
|
|
220
|
+
try {
|
|
221
|
+
await store.set({ key: record.key, value: entry });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
swap(was);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
/** Messages before `before`, oldest first, as many as fit `limit` and `PAGE_BUDGET`. */
|
|
229
|
+
async page(before, limit) {
|
|
230
|
+
const { records } = await load();
|
|
231
|
+
const end = before === undefined
|
|
232
|
+
? records.length
|
|
233
|
+
: records.findIndex((record) => record.entry.id === before);
|
|
234
|
+
const messages = [];
|
|
235
|
+
let bytes = 0;
|
|
236
|
+
let start = Math.max(end, 0);
|
|
237
|
+
while (start > 0 && messages.length < limit) {
|
|
238
|
+
const { entry } = records[start - 1];
|
|
239
|
+
const message = messageOf(entry);
|
|
240
|
+
bytes += size(message);
|
|
241
|
+
if (bytes > PAGE_BUDGET && messages.length > 0)
|
|
242
|
+
break;
|
|
243
|
+
messages.unshift(message);
|
|
244
|
+
start--;
|
|
245
|
+
}
|
|
246
|
+
return { messages, more: start > 0 };
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* A piece of work's record: its messages across the threads it follows, in the order it took them.
|
|
252
|
+
* Its id is the name of its own thread, whose messages are its unless they name another work; in
|
|
253
|
+
* any other thread, the messages that name it are. A message's place is its number in its own
|
|
254
|
+
* thread until the work follows a second thread, and a count kept with each message after. A
|
|
255
|
+
* message waiting in a thread it follows, said before the work had the thread, names no work yet:
|
|
256
|
+
* the work takes it, in the order it was said, before it keeps anything else there.
|
|
257
|
+
*/
|
|
258
|
+
export function createRecord(options) {
|
|
259
|
+
const { id } = options;
|
|
260
|
+
const placeOf = (record) => record.entry.place ?? record.seq;
|
|
261
|
+
const mine = (thread, record) => record.entry.work === undefined ? thread === id : record.entry.work === id;
|
|
262
|
+
/** Every thread it follows, read: what each holds, as it changes. */
|
|
263
|
+
const load = async () => {
|
|
264
|
+
const held = new Map();
|
|
265
|
+
for (const name of options.follows())
|
|
266
|
+
held.set(name, await options.thread(name).load());
|
|
267
|
+
return held;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Its own messages in the threads read, in order, as they stand now. Nothing waits between
|
|
271
|
+
* this and what is decided from it.
|
|
272
|
+
*/
|
|
273
|
+
const own = (held) => {
|
|
274
|
+
const records = [...held].flatMap(([thread, { records }]) => records.flatMap((record) => (mine(thread, record) ? [{ thread, record }] : [])));
|
|
275
|
+
// One thread's messages are in order already.
|
|
276
|
+
if (held.size > 1)
|
|
277
|
+
records.sort((a, b) => placeOf(a.record) - placeOf(b.record));
|
|
278
|
+
return records;
|
|
279
|
+
};
|
|
280
|
+
/** The highest place among its messages. */
|
|
281
|
+
const topOf = (records) => {
|
|
282
|
+
const last = records.at(-1);
|
|
283
|
+
return last ? placeOf(last.record) : 0;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Take the messages waiting in `thread` as its own, after all it holds: at once in memory, and
|
|
287
|
+
* `written` settles when each is stored, or rejects with one that isn't and stays waiting. Like
|
|
288
|
+
* a message being written, the model is sent none of them until all are stored.
|
|
289
|
+
*/
|
|
290
|
+
const take = (held, thread, joined) => {
|
|
291
|
+
const from = held.get(thread);
|
|
292
|
+
const waiting = thread === id ? [] : from.records.filter(({ entry }) => !entry.work);
|
|
293
|
+
let top = topOf(own(held));
|
|
294
|
+
const writes = waiting.map((record) => options.thread(thread).rewrite(from, record, {
|
|
295
|
+
...record.entry,
|
|
296
|
+
work: id,
|
|
297
|
+
place: ++top,
|
|
298
|
+
...(joined && record.entry.role === "user" ? { joined: true } : {}),
|
|
299
|
+
}));
|
|
300
|
+
for (const record of waiting)
|
|
301
|
+
record.writing = true;
|
|
302
|
+
const written = Promise.allSettled(writes).then((all) => {
|
|
303
|
+
for (const record of waiting)
|
|
304
|
+
record.writing = false;
|
|
305
|
+
const refused = all.find((one) => one.status === "rejected");
|
|
306
|
+
if (refused)
|
|
307
|
+
throw refused.reason;
|
|
308
|
+
});
|
|
309
|
+
return { taken: waiting.length, written };
|
|
310
|
+
};
|
|
311
|
+
/** Whether a user message of its follows the place `read`: one `stored` already, when asked. */
|
|
312
|
+
const saidAfter = (records, read, stored = false) => records.some(({ record }) => record.entry.role === "user" && placeOf(record) > read && !(stored && record.writing));
|
|
313
|
+
/** The thread of its newest user message: where it answers. Its own, with none. */
|
|
314
|
+
const whereOf = (records) => records.findLast(({ record }) => record.entry.role === "user")?.thread ?? id;
|
|
315
|
+
return {
|
|
316
|
+
id,
|
|
317
|
+
/**
|
|
318
|
+
* Keep a message of its in `thread`, one of those it follows, after any waiting there. `read`
|
|
319
|
+
* is given for what came of a model request that had read it up to that place: when a user
|
|
320
|
+
* message was said since, it is kept as `ahead` of it.
|
|
321
|
+
*/
|
|
322
|
+
async append(entry, at) {
|
|
323
|
+
const held = await load();
|
|
324
|
+
const { thread } = at;
|
|
325
|
+
// What waits there is taken and stored first, so this is never kept ahead of it: it joins
|
|
326
|
+
// a turn if this does, or comes of one. If the store won't take it, nothing is kept.
|
|
327
|
+
await take(held, thread, entry.joined === true || at.read !== undefined).written;
|
|
328
|
+
// From here to the push nothing waits, so its place and what is ahead of it are decided
|
|
329
|
+
// with the record as it stands.
|
|
330
|
+
const records = own(held);
|
|
331
|
+
const { record, stored } = options.thread(thread).push(held.get(thread), {
|
|
332
|
+
...entry,
|
|
333
|
+
...(thread === id ? {} : { work: id }),
|
|
334
|
+
...(held.size > 1 ? { place: topOf(records) + 1 } : {}),
|
|
335
|
+
...(at.read !== undefined && saidAfter(records, at.read) ? { ahead: at.read } : {}),
|
|
336
|
+
}, at.id);
|
|
337
|
+
await stored;
|
|
338
|
+
return { entry: record.entry, thread };
|
|
339
|
+
},
|
|
340
|
+
/**
|
|
341
|
+
* Take the messages waiting in `thread`, which it has come to follow, as its own: `joined`,
|
|
342
|
+
* when a turn of its is running. It says how many there were to answer: in its own thread,
|
|
343
|
+
* where nothing needs taking, those said since it last did anything.
|
|
344
|
+
*/
|
|
345
|
+
async adopt(thread, joined) {
|
|
346
|
+
const held = await load();
|
|
347
|
+
const { taken, written } = take(held, thread, joined);
|
|
348
|
+
await written;
|
|
349
|
+
if (thread !== id)
|
|
350
|
+
return taken;
|
|
351
|
+
const records = own(held);
|
|
352
|
+
const done = records.findLastIndex(({ record }) => record.entry.role !== "user");
|
|
353
|
+
return records.length - 1 - done;
|
|
354
|
+
},
|
|
355
|
+
/** Whether it holds anything but what was said to it: it has had a turn that did something. */
|
|
356
|
+
async begun() {
|
|
357
|
+
return own(await load()).some(({ record }) => record.entry.role !== "user");
|
|
358
|
+
},
|
|
359
|
+
/** Whether it has no message. */
|
|
360
|
+
async empty() {
|
|
361
|
+
return own(await load()).length === 0;
|
|
362
|
+
},
|
|
363
|
+
/** The thread it answers in now. */
|
|
364
|
+
async where() {
|
|
365
|
+
return whereOf(own(await load()));
|
|
366
|
+
},
|
|
367
|
+
/** Its first user message, for a title. */
|
|
368
|
+
async opening() {
|
|
369
|
+
const first = own(await load()).find(({ record }) => record.entry.role === "user");
|
|
370
|
+
return first ? textOf(first.record.entry) : "";
|
|
371
|
+
},
|
|
372
|
+
/**
|
|
373
|
+
* The conversation to send: the newest exchanges (each from the user message that starts a turn
|
|
374
|
+
* on) that fit the budget, and always the latest two, so "go on" after a long turn still has
|
|
375
|
+
* that turn. When those two are over the budget, their oldest function arguments and outputs
|
|
376
|
+
* are left out, each call still paired with an output. A message said during a turn comes after
|
|
377
|
+
* whatever is `ahead` of it, so the model reads it as not yet answered. `read` is the place up
|
|
378
|
+
* to which every user message was read, for `append` and `saidAfter`: one still being written
|
|
379
|
+
* is not read yet. `thread` is where what comes of it is kept.
|
|
380
|
+
*/
|
|
381
|
+
async input() {
|
|
382
|
+
const records = own(await load());
|
|
383
|
+
const last = topOf(records);
|
|
384
|
+
const unwritten = records.find(({ record }) => record.writing);
|
|
385
|
+
const read = unwritten ? placeOf(unwritten.record) - 1 : last;
|
|
386
|
+
const exchanges = [];
|
|
387
|
+
/** Said during a turn, and not yet put: what is ahead of them goes first. */
|
|
388
|
+
const late = [];
|
|
389
|
+
const put = (entry) => {
|
|
390
|
+
if ((entry.role === "user" && !entry.joined) || exchanges.length === 0)
|
|
391
|
+
exchanges.push([]);
|
|
392
|
+
exchanges.at(-1)?.push(...entry.items);
|
|
393
|
+
};
|
|
394
|
+
/** Put the late messages placed up to `read`. */
|
|
395
|
+
const putLate = (read) => {
|
|
396
|
+
while (late[0] && placeOf(late[0]) <= read)
|
|
397
|
+
put(late.shift().entry);
|
|
398
|
+
};
|
|
399
|
+
for (const { record } of records) {
|
|
400
|
+
// Nothing to send: a user message still being written, or a line with no items.
|
|
401
|
+
if (record.writing || record.entry.items.length === 0)
|
|
402
|
+
continue;
|
|
403
|
+
if (record.entry.joined) {
|
|
404
|
+
late.push(record);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
putLate(record.entry.ahead ?? last);
|
|
408
|
+
put(record.entry);
|
|
409
|
+
}
|
|
410
|
+
putLate(last);
|
|
411
|
+
let total = exchanges.reduce((sum, items) => sum + size(items), 0);
|
|
412
|
+
while (exchanges.length > 2 && total > INPUT_BUDGET)
|
|
413
|
+
total -= size(exchanges.shift());
|
|
414
|
+
const items = exchanges.flat();
|
|
415
|
+
for (let i = 0; i < items.length && total > INPUT_BUDGET; i++) {
|
|
416
|
+
const item = items[i];
|
|
417
|
+
if (item.type === "function_call_output" && item.output !== LEFT_OUT) {
|
|
418
|
+
total -= size(item.output) - size(LEFT_OUT);
|
|
419
|
+
items[i] = { ...item, output: LEFT_OUT };
|
|
420
|
+
}
|
|
421
|
+
else if (item.type === "function_call" && item.arguments !== ARGUMENTS_LEFT_OUT) {
|
|
422
|
+
total -= size(item.arguments) - size(ARGUMENTS_LEFT_OUT);
|
|
423
|
+
items[i] = { ...item, arguments: ARGUMENTS_LEFT_OUT };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { items, read, thread: whereOf(records.filter(({ record }) => !record.writing)) };
|
|
427
|
+
},
|
|
428
|
+
/** Whether a user message of its has been stored after the place `read`, which `input` gave. */
|
|
429
|
+
async saidAfter(read) {
|
|
430
|
+
return saidAfter(own(await load()), read, true);
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Running the functions a model calls, and what is kept of their output. */
|
|
2
|
+
import { type Outcome, type Tool } from "./types.js";
|
|
3
|
+
/** A function's output keeps this many characters, so one result can't fill a request. */
|
|
4
|
+
export declare const MAX_OUTPUT = 30000;
|
|
5
|
+
/**
|
|
6
|
+
* `text`, or its first and last `MAX_OUTPUT / 2` characters with what was cut between them: a
|
|
7
|
+
* failing command's error is at the end, and what it was doing at the start.
|
|
8
|
+
*/
|
|
9
|
+
export declare function capped(text: string): string;
|
|
10
|
+
/** Run a function call: the result, or the refusal, as the function's output. */
|
|
11
|
+
export declare function runTool(tools: Tool[], name: string, args: string): Promise<Outcome>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Running the functions a model calls, and what is kept of their output. */
|
|
2
|
+
import { codeOf } from "./types.js";
|
|
3
|
+
/** A function's output keeps this many characters, so one result can't fill a request. */
|
|
4
|
+
export const MAX_OUTPUT = 30_000;
|
|
5
|
+
/**
|
|
6
|
+
* `text`, or its first and last `MAX_OUTPUT / 2` characters with what was cut between them: a
|
|
7
|
+
* failing command's error is at the end, and what it was doing at the start.
|
|
8
|
+
*/
|
|
9
|
+
export function capped(text) {
|
|
10
|
+
if (text.length <= MAX_OUTPUT)
|
|
11
|
+
return text;
|
|
12
|
+
// Between characters: never half of a surrogate pair at either cut.
|
|
13
|
+
const head = text.slice(0, MAX_OUTPUT / 2).replace(/[\uD800-\uDBFF]$/, "");
|
|
14
|
+
const tail = text.slice(-MAX_OUTPUT / 2).replace(/^[\uDC00-\uDFFF]/, "");
|
|
15
|
+
return `${head}\n… [${text.length - head.length - tail.length} characters cut] …\n${tail}`;
|
|
16
|
+
}
|
|
17
|
+
const failure = ({ code, message }) => JSON.stringify({ error: { code, message } });
|
|
18
|
+
/** Run a function call: the result, or the refusal, as the function's output. */
|
|
19
|
+
export async function runTool(tools, name, args) {
|
|
20
|
+
const refused = (code, message) => ({
|
|
21
|
+
output: failure({ code, message }),
|
|
22
|
+
ok: false,
|
|
23
|
+
code,
|
|
24
|
+
});
|
|
25
|
+
const found = tools.find((tool) => tool.name === name);
|
|
26
|
+
if (!found)
|
|
27
|
+
return refused("unknown_tool", `No tool is named ${name.slice(0, 64)}.`);
|
|
28
|
+
let input;
|
|
29
|
+
try {
|
|
30
|
+
input = args.trim() === "" ? {} : JSON.parse(args);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return refused("invalid", "The arguments aren't JSON.");
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return await found.run(input);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const { code, message } = codeOf(error);
|
|
40
|
+
return refused(code, message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the looper is given and what it gives back, in its own words. Whoever hosts it maps its own
|
|
3
|
+
* world onto these: the looper names no interface, no chat and no agent.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
/** A JSON value. */
|
|
7
|
+
export type Json = string | number | boolean | null | Json[] | {
|
|
8
|
+
[key: string]: Json;
|
|
9
|
+
};
|
|
10
|
+
/** One item of a conversation as a model reads it: something said, a function call, or its output. */
|
|
11
|
+
export declare const Item: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
12
|
+
type: z.ZodLiteral<"message">;
|
|
13
|
+
role: z.ZodEnum<{
|
|
14
|
+
assistant: "assistant";
|
|
15
|
+
user: "user";
|
|
16
|
+
}>;
|
|
17
|
+
content: z.ZodString;
|
|
18
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
19
|
+
type: z.ZodLiteral<"function_call">;
|
|
20
|
+
callId: z.ZodString;
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
arguments: z.ZodString;
|
|
23
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
24
|
+
type: z.ZodLiteral<"function_call_output">;
|
|
25
|
+
callId: z.ZodString;
|
|
26
|
+
output: z.ZodString;
|
|
27
|
+
}, z.core.$strip>], "type">;
|
|
28
|
+
export type Item = z.output<typeof Item>;
|
|
29
|
+
/** What a model gives back: something it says, or a function it calls. */
|
|
30
|
+
export type OutputItem = Exclude<Item, {
|
|
31
|
+
type: "function_call_output";
|
|
32
|
+
}>;
|
|
33
|
+
/** A function the model may call, as a request describes it. */
|
|
34
|
+
export interface ToolSpec {
|
|
35
|
+
name: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
/** The JSON Schema of its arguments. */
|
|
38
|
+
parameters: Record<string, Json>;
|
|
39
|
+
}
|
|
40
|
+
/** How a function call ended, as the model and the record see it. */
|
|
41
|
+
export interface Outcome {
|
|
42
|
+
output: string;
|
|
43
|
+
ok: boolean;
|
|
44
|
+
/** Why it didn't, when `ok` is false. */
|
|
45
|
+
code?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A function the model may call, with the way to run it. `run` gets the call's arguments, parsed;
|
|
49
|
+
* what it throws is the call's output, as a refusal: with its code when it is a `Refusal`.
|
|
50
|
+
*/
|
|
51
|
+
export interface Tool extends ToolSpec {
|
|
52
|
+
run(input: unknown): Promise<Outcome>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The model: one stateless answer to a whole conversation. A request that can't be answered rejects,
|
|
56
|
+
* with a `Refusal` when there is a code to show. Text it writes while it answers is given to the
|
|
57
|
+
* looper's `delta`, with the request's id.
|
|
58
|
+
*/
|
|
59
|
+
export interface Model {
|
|
60
|
+
respond(request: {
|
|
61
|
+
requestId: string;
|
|
62
|
+
model?: string;
|
|
63
|
+
instructions: string;
|
|
64
|
+
input: Item[];
|
|
65
|
+
tools: ToolSpec[];
|
|
66
|
+
}): Promise<{
|
|
67
|
+
output: OutputItem[];
|
|
68
|
+
}>;
|
|
69
|
+
/** Stop the answer to `requestId`, if it is still being made; its `respond` then rejects. */
|
|
70
|
+
cancel(request: {
|
|
71
|
+
requestId: string;
|
|
72
|
+
}): Promise<unknown>;
|
|
73
|
+
}
|
|
74
|
+
/** Where the looper keeps its records: values by key, listed in key order a page at a time. */
|
|
75
|
+
export interface Store {
|
|
76
|
+
get(input: {
|
|
77
|
+
key: string;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
value?: Json | undefined;
|
|
80
|
+
}>;
|
|
81
|
+
set(input: {
|
|
82
|
+
key: string;
|
|
83
|
+
value: Json;
|
|
84
|
+
}): Promise<unknown>;
|
|
85
|
+
delete(input: {
|
|
86
|
+
key: string;
|
|
87
|
+
}): Promise<unknown>;
|
|
88
|
+
list(input: {
|
|
89
|
+
prefix?: string;
|
|
90
|
+
after?: string;
|
|
91
|
+
limit?: number;
|
|
92
|
+
}): Promise<{
|
|
93
|
+
keys: string[];
|
|
94
|
+
next: string | null;
|
|
95
|
+
}>;
|
|
96
|
+
}
|
|
97
|
+
/** Where the looper says what an operator may want to know; nothing a thread shows. */
|
|
98
|
+
export interface Log {
|
|
99
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
100
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
101
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A message as a thread shows it. A `tool` message tells what the loop did: with `tool`, the call
|
|
105
|
+
* it stands for, said as `running` when it starts and again under the same id when it ends;
|
|
106
|
+
* without, its `text` says it in one line, as for calls that never ran or a turn that goes on
|
|
107
|
+
* after it was cut off.
|
|
108
|
+
*/
|
|
109
|
+
export interface Shown {
|
|
110
|
+
id: string;
|
|
111
|
+
role: "user" | "assistant" | "tool";
|
|
112
|
+
text: string;
|
|
113
|
+
/** An ISO 8601 time. */
|
|
114
|
+
at: string;
|
|
115
|
+
tool?: {
|
|
116
|
+
name: string;
|
|
117
|
+
state: "running" | "done" | "refused";
|
|
118
|
+
input?: string;
|
|
119
|
+
output?: string;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** What a loop is doing. On `error`, `detail.code` is the refusal's code. */
|
|
123
|
+
export type Status = {
|
|
124
|
+
state: "idle" | "thinking" | "tool";
|
|
125
|
+
} | {
|
|
126
|
+
state: "error";
|
|
127
|
+
detail: {
|
|
128
|
+
code: string;
|
|
129
|
+
message: string;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Something refused, with a stable code a host can show or map. A host that refuses for a reason of
|
|
134
|
+
* its own can keep that reason as the `cause`.
|
|
135
|
+
*/
|
|
136
|
+
export declare class Refusal extends Error {
|
|
137
|
+
readonly name = "Refusal";
|
|
138
|
+
readonly code: string;
|
|
139
|
+
constructor(code: string, message: string, options?: {
|
|
140
|
+
cause?: unknown;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** The code and message of anything a call or a turn can throw. */
|
|
144
|
+
export declare function codeOf(error: unknown): {
|
|
145
|
+
code: string;
|
|
146
|
+
message: string;
|
|
147
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the looper is given and what it gives back, in its own words. Whoever hosts it maps its own
|
|
3
|
+
* world onto these: the looper names no interface, no chat and no agent.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
/** What one text of a model request can hold, and one name in it. */
|
|
7
|
+
const Text = z.string().max(200_000);
|
|
8
|
+
const Name = z.string().min(1).max(256);
|
|
9
|
+
/** One item of a conversation as a model reads it: something said, a function call, or its output. */
|
|
10
|
+
export const Item = z.discriminatedUnion("type", [
|
|
11
|
+
z.object({ type: z.literal("message"), role: z.enum(["user", "assistant"]), content: Text }),
|
|
12
|
+
z.object({ type: z.literal("function_call"), callId: Name, name: Name, arguments: Text }),
|
|
13
|
+
z.object({ type: z.literal("function_call_output"), callId: Name, output: Text }),
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Something refused, with a stable code a host can show or map. A host that refuses for a reason of
|
|
17
|
+
* its own can keep that reason as the `cause`.
|
|
18
|
+
*/
|
|
19
|
+
export class Refusal extends Error {
|
|
20
|
+
name = "Refusal";
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message, options) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The code and message of anything a call or a turn can throw. */
|
|
28
|
+
export function codeOf(error) {
|
|
29
|
+
if (error instanceof Refusal)
|
|
30
|
+
return { code: error.code, message: error.message };
|
|
31
|
+
return { code: "failed", message: "The call failed." };
|
|
32
|
+
}
|