@agentorg/cli 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +26 -12
- package/dist/node_modules/@agentorg/looper/dist/src/loop.js +72 -38
- package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +27 -17
- package/dist/node_modules/@agentorg/looper/dist/src/looper.js +405 -113
- 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 +97 -15
- package/dist/node_modules/@agentorg/looper/dist/src/record.js +211 -56
- package/dist/node_modules/@agentorg/manager/dist/src/control.d.ts +1 -1
- package/dist/node_modules/@agentorg/manager/dist/src/control.js +1 -1
- package/dist/node_modules/@agentorg/shell/dist/app.css +47 -111
- package/dist/node_modules/@agentorg/shell/dist/app.js +139 -139
- package/dist/node_modules/assistant/dist/src/chats.d.ts +8 -6
- package/dist/node_modules/assistant/dist/src/chats.js +23 -25
- package/dist/node_modules/assistant/dist/src/engine.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ AgentOrg creates and manages agents. Each part is a program that joins a Hub as
|
|
|
20
20
|
- `@agentorg/mcp`: the built-in MCP provider, an external Actor exporting `unit.provider@1` as `provider`, and the only code that imports the MCP client (`@modelcontextprotocol/client`). `startMcpProvider({hubUrl, actorId, credential, dir})` runs it; its slot `actors` is bound to `hub.actors@1`. It offers two kinds: `url` attaches to a server over Streamable HTTP, sending `secrets.token` as a bearer token on every request (only over https or to this machine), and `command` starts a local server over stdio, with the basic environment and each of its `secrets` as an environment variable. Each connection joins the Hub as its own Actor exporting `tools@1` as `tools`: `list` and `call` go to the server, a tool's result comes back as text, with `failed` when the tool reported an error, and `changed` fires when the server's tools change or it goes or comes back. While a server is away `list` and `call` are refused `not_connected`, and the provider tries again after a backoff from 1 s doubling to 60 s (starting over after 10 minutes up). `create`'s `secrets` are write-only: the values cross once and go only to the provider's private `dir`, never into `params`, `target`, `detail`, status or a log (what a server prints is scrubbed of them). Each connection's Hub credential, parameters and secrets are kept in that folder, and connections rejoin at its next start. A server started by `command` is an ordinary process with the person's rights, outside the Hub's sandbox.
|
|
21
21
|
- `@agentorg/manager`: management, an external Actor exporting `agents@1`, `connections@1`, `skills@1` and, as `settings`, `agent.settings@1`. `startManager({hubUrl, actorId, credential, dir})` runs it; bound to `hub.actors@1`, `hub.grants@1` and `hub.bindings@1`, it is a manager. A provider of either sort is connected by exposing its export to it: the Hub's slots are the only list. It keeps what the Hub can't know, in its private `dir`: each connection's origin, each agent's provider, creatable, instructions and skills, and the skills as `SKILL.md` folders. Status is read when asked, what a connection is exposed to is the Hub's bindings, and `secrets` go straight to the provider. `agents.create` is all or none: the provider makes the agent, its `settings` are bound, its skills added (one whose name is here keeps the one here), each connection it needs made or picked and exposed under its slot, and its chats exposed to the person; if a step fails, what this creation made is removed. The Hub drops that exposure when an agent comes back offering its chats in another shape, as after an upgrade, so management makes it again each time an agent connects. An agent reads only its own instructions and skills, and only it hears that they changed. `agents.models` and `agents.setModel` pass to the agent's provider, as `start` and `stop` do; stopping an agent is the one way to stop its work.
|
|
22
22
|
- `chatgpt`: the model, a program. It signs in with ChatGPT for the person's plan (`account.login@1`: OpenAI's OAuth for open-source apps, with dynamic registration, PKCE and a verified id_token; refresh tokens rotate, one refresh at a time) and serves `llm.session@1` on that plan through the Responses API, streamed and stored nowhere. The tokens never leave it: never in a reply, an event, a log or a refusal. It never uses an API key or API billing: a usage limit is refused `usage_limit`, with where to manage it. `startChatGPT({hubUrl, actorId, credential, dir})` runs it as an Actor exporting `login` and `session` with no slots at all: the sign-in is in `sign-in.json` in its private `dir` (mode 0600, written through before a change is acknowledged, since a rotated refresh token can't be had again), and it reaches OpenAI over the network directly, https only, with redirects returned as they are. Its tests run against an in-process fake of OpenAI, so none reaches OpenAI.
|
|
23
|
-
- `@agentorg/looper`: an agent's loops, knowing nothing of what hosts them. `createLooper` is given a model client, the tools a request may call, a store for its records and where to say what it does, each as a plain type of its own, and takes a message with the name of the thread it came in on.
|
|
24
|
-
- `assistant`: the agent engine and the bundled agent provider. The engine binds the looper to one agent: a chat is a thread, the model and the tools are the agent's slots, and the records go where the agent's are kept. So an agent works
|
|
23
|
+
- `@agentorg/looper`: an agent's loops, knowing nothing of what hosts them. `createLooper` is given a model client, the tools a request may call, a store for its records and where to say what it does, each as a plain type of its own, and takes a message with the name of the thread it came in on. What answers is a piece of work, with a loop of its own: a turn sends the work's messages and the tools to the model, runs what the model calls, and goes on until the model answers or the turn has made 200 calls. A message in a thread that a piece of work follows is given to that work, in code. One in a thread nothing follows is recorded and said at once, and put to the main loop, which makes one model request with one function, `route`, and no other tool: it is told the work that is in a turn now, by id and title, and what has come in (the newest eight messages waiting in the thread, the first 4,000 characters of each), and says that is new work, with a title, or points at one of those, which then follows that thread too. It is told to give a message to work in a turn only when the message points at that work explicitly. It decides for one thread at a time, on all that waits in the thread, keeps nothing between decisions and answers nobody; an answer that calls nothing, or names no work in a turn, starts new work. A message is kept once, in the thread it is shown in; a piece of work reads its own across the threads it follows, in the order it took them, and what comes of a model request is written in the thread of the newest message that request read. Turns of different pieces of work run side by side, four at once, and one more that only work that has already done something may take; a turn past that waits for a place, and while such work keeps five turns running new work goes on waiting. Sixteen turns may wait; past that a message that would start a turn is refused `busy`, or, if it is new work's first, kept and told to have failed `busy`. The main loop takes no place. A message said to work whose turn is running or waiting is recorded and said at once, and that turn takes it at its next model request. A turn that fails tells every thread it had taken a message in. A piece of work is marked in the store while a turn of it hasn't ended, and a thread while messages wait in it: `resume` goes on, once, with each that was cut off by a looper closing or going without closing, and the model of a turn that goes on is told to check what is already done. Messages the main loop couldn't place, or was stopped over, wait for their thread's next message, and all of them are put to it together. Stopped, it cuts every turn and the main loop, and takes no message until it is started. It imports no other package of this repository, and a test of its own fails if a file under its `src` does.
|
|
24
|
+
- `assistant`: the agent engine and the bundled agent provider. The engine binds the looper to one agent: a chat is a thread, the model and the tools are the agent's slots, and the records go where the agent's are kept. A new chat's first message is read by the agent's main loop, which starts new work for it or, when it points at work in a turn, gives it to that work, which reads it with all it holds; a piece of work answers in the chat of the newest message it has read, and the chat is that work's from then on. The main loop's request is the looper's own words and one function: none of the agent's instructions, skills or tools. So an agent works on several things at once, as the looper's places allow. `startAgentProvider` runs an external Actor exporting `agent.provider@1` as `provider`, which offers one creatable, Assistant. Each agent is its own Actor on its own channel in that process, exporting `chat@1`, with the model behind `model: llm.session@1`, instructions and skills from an optional `settings: agent.settings@1`, and tools from each `tools@1` slot exposed to it. Its chats and model choice stay in the provider's private folder; the model is chosen through the provider's `models` and `setModel`, which name the agent. The agent is `working` while its main loop decides or any work's turn runs or waits. A stopped agent stays on its channel and runs no turn: its chats can still be read, and a message is refused `busy`. A message sent in a chat whose work is in a turn is recorded and shown at once, and that turn takes it at its next model request. A message that gets no answer leaves its chat with a `problem`, until that chat's next reply, and a message sent after it is read with what went unanswered. A turn that a restart cut off goes on once when the agent is back, the chat of the message it was answering showing one line that says so and its model told to check what is already done; one that ended, failed or was stopped doesn't.
|
|
25
25
|
|
|
26
26
|
## Errors
|
|
27
27
|
|
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
* A loop: a turn sends its record and the tools to the model, runs each function the model calls,
|
|
3
3
|
* and sends the results back, until the model answers without calling anything, or the turn has
|
|
4
4
|
* made its `MAX_CALLS` calls. A message said while a turn runs is recorded at once, and the turn
|
|
5
|
-
* takes it at its next model request.
|
|
5
|
+
* takes it at its next model request. A loop is one piece of work's: its messages come in the
|
|
6
|
+
* threads the work follows, and what comes of a request is kept in the thread of the newest
|
|
7
|
+
* message that request read.
|
|
6
8
|
*/
|
|
7
9
|
import { type LoopRecord } from "./record.js";
|
|
8
10
|
import { type Log, type Model, type Shown, type Status, type Tool } from "./types.js";
|
|
9
11
|
/** A brake on a turn, not a budget: nothing approves a call, and the person can stop it. */
|
|
10
12
|
export declare const MAX_CALLS = 200;
|
|
11
|
-
/** Where a loop says what it does. */
|
|
13
|
+
/** Where a loop says what it does, each message with the thread it is shown in. */
|
|
12
14
|
export interface LoopOut {
|
|
13
|
-
message(message: Shown): void;
|
|
15
|
+
message(thread: string, message: Shown): void;
|
|
14
16
|
/** Text of the reply `id` as the model writes it. */
|
|
15
|
-
delta(id: string, text: string): void;
|
|
17
|
+
delta(thread: string, id: string, text: string): void;
|
|
18
|
+
/** What it is doing. */
|
|
16
19
|
status(status: Status): void;
|
|
20
|
+
/** A message in `thread` got no answer, and why: said before the `error` status. */
|
|
21
|
+
failed(thread: string, problem: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}): void;
|
|
17
25
|
}
|
|
18
26
|
/** A turn's place among the turns running: `entered` settles when it has it, or has left. */
|
|
19
27
|
export interface Place {
|
|
@@ -42,18 +50,24 @@ export declare function createLoop(options: {
|
|
|
42
50
|
log: Log;
|
|
43
51
|
}): {
|
|
44
52
|
/**
|
|
45
|
-
* Say a message. With no turn it starts one, whose progress is
|
|
46
|
-
* running or waiting for its place, it is recorded and said at
|
|
47
|
-
* its next model request.
|
|
53
|
+
* Say a message in a thread the work follows. With no turn it starts one, whose progress is
|
|
54
|
+
* said through `out`. In a turn, running or waiting for its place, it is recorded and said at
|
|
55
|
+
* once, and the turn takes it at its next model request.
|
|
48
56
|
*/
|
|
49
|
-
send: (text: string) => Promise<{
|
|
57
|
+
send: (text: string, thread: string) => Promise<{
|
|
50
58
|
messageId: string;
|
|
51
59
|
}>;
|
|
52
60
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* a
|
|
56
|
-
|
|
61
|
+
* Take the messages waiting in `thread`, which the work has come to follow, as its own, and
|
|
62
|
+
* answer them: in the turn running, or in one this starts. With none waiting no turn starts.
|
|
63
|
+
* When a cancel came first nothing is taken, and it says so with `false`.
|
|
64
|
+
*/
|
|
65
|
+
adopt: (thread: string) => Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Go on with a turn that was cut off before it ended, as by a restart: the thread it was
|
|
68
|
+
* answering in shows one line saying so, and the model is told once to check what is already
|
|
69
|
+
* done. Nothing goes on in work with nothing kept, in work that has had a turn since the loop
|
|
70
|
+
* was made, or when a cancel comes first.
|
|
57
71
|
*/
|
|
58
72
|
resume: () => Promise<void>;
|
|
59
73
|
/** Whether the turn running now goes on from one that was cut off. */
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* A loop: a turn sends its record and the tools to the model, runs each function the model calls,
|
|
3
3
|
* and sends the results back, until the model answers without calling anything, or the turn has
|
|
4
4
|
* made its `MAX_CALLS` calls. A message said while a turn runs is recorded at once, and the turn
|
|
5
|
-
* takes it at its next model request.
|
|
5
|
+
* takes it at its next model request. A loop is one piece of work's: its messages come in the
|
|
6
|
+
* threads the work follows, and what comes of a request is kept in the thread of the newest
|
|
7
|
+
* message that request read.
|
|
6
8
|
*/
|
|
7
|
-
import { argumentsOf, head, messageOf } from "./record.js";
|
|
9
|
+
import { argumentsOf, head, messageOf, userEntry } from "./record.js";
|
|
8
10
|
import { capped, runTool } from "./tools.js";
|
|
9
11
|
import { codeOf, Refusal, } from "./types.js";
|
|
10
12
|
/** A brake on a turn, not a budget: nothing approves a call, and the person can stop it. */
|
|
@@ -55,7 +57,11 @@ export function createLoop(options) {
|
|
|
55
57
|
if (current.cancelled)
|
|
56
58
|
return;
|
|
57
59
|
log.error("A turn failed.", { error: String(error) });
|
|
58
|
-
|
|
60
|
+
// Every thread it took a message in, and the one it was answering in, is told.
|
|
61
|
+
const detail = codeOf(error);
|
|
62
|
+
for (const thread of new Set([...current.took, current.thread ?? record.id]))
|
|
63
|
+
out.failed(thread, detail);
|
|
64
|
+
status(current, { state: "error", detail });
|
|
59
65
|
})
|
|
60
66
|
.finally(() => {
|
|
61
67
|
current.place?.leave();
|
|
@@ -65,9 +71,9 @@ export function createLoop(options) {
|
|
|
65
71
|
.catch((error) => log.error("A turn's status failed.", { error: String(error) }));
|
|
66
72
|
lastEnded = current.ended;
|
|
67
73
|
};
|
|
68
|
-
const show = (entry) => out.message(messageOf(entry));
|
|
74
|
+
const show = ({ entry, thread }) => out.message(thread, messageOf(entry));
|
|
69
75
|
/** A call starting: shown as running, under the id its record will have when it ends. */
|
|
70
|
-
const running = (call, id) => out.message({
|
|
76
|
+
const running = (call, id, thread) => out.message(thread, {
|
|
71
77
|
id,
|
|
72
78
|
role: "tool",
|
|
73
79
|
text: `${call.name}: running`,
|
|
@@ -78,7 +84,7 @@ export function createLoop(options) {
|
|
|
78
84
|
* Record a function call with its output, keeping every call paired, as the API needs. Its end
|
|
79
85
|
* is shown even if the record can't be written, so no call is left shown as running.
|
|
80
86
|
*/
|
|
81
|
-
const answered = async (call, result, id, read) => {
|
|
87
|
+
const answered = async (call, result, id, read, thread) => {
|
|
82
88
|
const entry = {
|
|
83
89
|
role: "tool",
|
|
84
90
|
text: `${call.name}: ${result.ok ? "done" : result.code}`,
|
|
@@ -93,10 +99,10 @@ export function createLoop(options) {
|
|
|
93
99
|
],
|
|
94
100
|
};
|
|
95
101
|
try {
|
|
96
|
-
show(await record.append(entry, id, read));
|
|
102
|
+
show(await record.append(entry, { thread, id, read }));
|
|
97
103
|
}
|
|
98
104
|
catch (error) {
|
|
99
|
-
out.message(messageOf({
|
|
105
|
+
out.message(thread, messageOf({
|
|
100
106
|
...entry,
|
|
101
107
|
id,
|
|
102
108
|
at: new Date().toISOString(),
|
|
@@ -109,19 +115,20 @@ export function createLoop(options) {
|
|
|
109
115
|
* Note calls that won't run, in one message however many there are. They never ran, so the
|
|
110
116
|
* record leaves them out: what the model is sent next holds only calls with their outputs.
|
|
111
117
|
*/
|
|
112
|
-
const skipped = async (calls, code) => {
|
|
118
|
+
const skipped = async (calls, code, thread) => {
|
|
113
119
|
if (calls.length === 0)
|
|
114
120
|
return;
|
|
115
121
|
const text = calls.length === 1 ? `${calls[0]?.name}: ${code}` : `${calls.length} calls: ${code}`;
|
|
116
|
-
show(await record.append({ role: "tool", text, items: [] }));
|
|
122
|
+
show(await record.append({ role: "tool", text, items: [] }, { thread }));
|
|
117
123
|
};
|
|
118
124
|
async function run(current) {
|
|
119
125
|
let calls = 0;
|
|
120
126
|
for (;;) {
|
|
121
127
|
const { instructions, tools, model: chosen } = await options.request();
|
|
122
|
-
const { items: input, read } = await record.input();
|
|
128
|
+
const { items: input, read, thread } = await record.input();
|
|
123
129
|
if (current.cancelled)
|
|
124
130
|
return;
|
|
131
|
+
current.thread = thread;
|
|
125
132
|
const requestId = crypto.randomUUID();
|
|
126
133
|
const replyId = crypto.randomUUID();
|
|
127
134
|
current.requestId = requestId;
|
|
@@ -147,7 +154,7 @@ export function createLoop(options) {
|
|
|
147
154
|
show(await record.append({
|
|
148
155
|
role: "assistant",
|
|
149
156
|
items: texts.map((content) => ({ type: "message", role: "assistant", content })),
|
|
150
|
-
}, replyId, read));
|
|
157
|
+
}, { thread, id: replyId, read }));
|
|
151
158
|
if (called.length === 0) {
|
|
152
159
|
if (current.cancelled)
|
|
153
160
|
return;
|
|
@@ -174,7 +181,7 @@ export function createLoop(options) {
|
|
|
174
181
|
for (const [i, call] of called.entries()) {
|
|
175
182
|
if (current.cancelled || calls >= MAX_CALLS) {
|
|
176
183
|
const rest = called.slice(i);
|
|
177
|
-
await skipped(rest, current.cancelled ? "cancelled" : "limit");
|
|
184
|
+
await skipped(rest, current.cancelled ? "cancelled" : "limit", thread);
|
|
178
185
|
break;
|
|
179
186
|
}
|
|
180
187
|
calls++;
|
|
@@ -182,9 +189,9 @@ export function createLoop(options) {
|
|
|
182
189
|
// A call that has started runs to its end, and its real result is kept: it may have
|
|
183
190
|
// taken effect.
|
|
184
191
|
const id = crypto.randomUUID();
|
|
185
|
-
running(call, id);
|
|
192
|
+
running(call, id, thread);
|
|
186
193
|
const result = await runTool(tools, call.name, call.arguments);
|
|
187
|
-
await answered(call, result, id, read);
|
|
194
|
+
await answered(call, result, id, read, thread);
|
|
188
195
|
}
|
|
189
196
|
if (current.cancelled)
|
|
190
197
|
return;
|
|
@@ -200,6 +207,8 @@ export function createLoop(options) {
|
|
|
200
207
|
current.waiting = false;
|
|
201
208
|
if (current.cancelled)
|
|
202
209
|
return;
|
|
210
|
+
// Where it answers until a request says: so a failure before one names its thread.
|
|
211
|
+
current.thread = await record.where();
|
|
203
212
|
await run(current);
|
|
204
213
|
}
|
|
205
214
|
/**
|
|
@@ -212,14 +221,10 @@ export function createLoop(options) {
|
|
|
212
221
|
current.place = options.enter(begun);
|
|
213
222
|
current.waiting = true;
|
|
214
223
|
};
|
|
215
|
-
const said = async (text, joined) => {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
...(joined ? { joined: true } : {}),
|
|
220
|
-
});
|
|
221
|
-
show(entry);
|
|
222
|
-
return { messageId: entry.id };
|
|
224
|
+
const said = async (text, thread, joined) => {
|
|
225
|
+
const kept = await record.append({ ...userEntry(text), ...(joined ? { joined: true } : {}) }, { thread });
|
|
226
|
+
show(kept);
|
|
227
|
+
return { messageId: kept.entry.id };
|
|
223
228
|
};
|
|
224
229
|
/**
|
|
225
230
|
* Start a turn: `open` takes its place and keeps what opens it, then its loop runs. When `open`
|
|
@@ -227,7 +232,7 @@ export function createLoop(options) {
|
|
|
227
232
|
* runs nothing.
|
|
228
233
|
*/
|
|
229
234
|
const start = async (cancelled, open) => {
|
|
230
|
-
const current = { cancelled };
|
|
235
|
+
const current = { cancelled, took: new Set() };
|
|
231
236
|
if (!cancelled)
|
|
232
237
|
turn = current;
|
|
233
238
|
began = true;
|
|
@@ -251,23 +256,49 @@ export function createLoop(options) {
|
|
|
251
256
|
};
|
|
252
257
|
return {
|
|
253
258
|
/**
|
|
254
|
-
* Say a message. With no turn it starts one, whose progress is
|
|
255
|
-
* running or waiting for its place, it is recorded and said at
|
|
256
|
-
* its next model request.
|
|
259
|
+
* Say a message in a thread the work follows. With no turn it starts one, whose progress is
|
|
260
|
+
* said through `out`. In a turn, running or waiting for its place, it is recorded and said at
|
|
261
|
+
* once, and the turn takes it at its next model request.
|
|
257
262
|
*/
|
|
258
|
-
send: (text) => inOrder(async (cancelled) => {
|
|
259
|
-
if (turn)
|
|
260
|
-
|
|
263
|
+
send: (text, thread) => inOrder(async (cancelled) => {
|
|
264
|
+
if (turn) {
|
|
265
|
+
turn.took.add(thread);
|
|
266
|
+
return said(text, thread, true);
|
|
267
|
+
}
|
|
261
268
|
return (await start(cancelled, async (current) => {
|
|
262
|
-
enter(current,
|
|
263
|
-
|
|
269
|
+
enter(current, await record.begun());
|
|
270
|
+
current.took.add(thread);
|
|
271
|
+
return { opened: await said(text, thread, false) };
|
|
264
272
|
}));
|
|
265
273
|
}),
|
|
266
274
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* a
|
|
270
|
-
|
|
275
|
+
* Take the messages waiting in `thread`, which the work has come to follow, as its own, and
|
|
276
|
+
* answer them: in the turn running, or in one this starts. With none waiting no turn starts.
|
|
277
|
+
* When a cancel came first nothing is taken, and it says so with `false`.
|
|
278
|
+
*/
|
|
279
|
+
adopt: (thread) => inOrder(async (cancelled) => {
|
|
280
|
+
if (cancelled)
|
|
281
|
+
return false;
|
|
282
|
+
if (turn) {
|
|
283
|
+
turn.took.add(thread);
|
|
284
|
+
await record.adopt(thread, true);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
await start(false, async (current) => {
|
|
288
|
+
enter(current, await record.begun());
|
|
289
|
+
current.took.add(thread);
|
|
290
|
+
if ((await record.adopt(thread, false)) > 0)
|
|
291
|
+
return { opened: undefined };
|
|
292
|
+
current.place?.leave();
|
|
293
|
+
return undefined;
|
|
294
|
+
});
|
|
295
|
+
return true;
|
|
296
|
+
}),
|
|
297
|
+
/**
|
|
298
|
+
* Go on with a turn that was cut off before it ended, as by a restart: the thread it was
|
|
299
|
+
* answering in shows one line saying so, and the model is told once to check what is already
|
|
300
|
+
* done. Nothing goes on in work with nothing kept, in work that has had a turn since the loop
|
|
301
|
+
* was made, or when a cancel comes first.
|
|
271
302
|
*/
|
|
272
303
|
resume: () => inOrder(async (cancelled) => {
|
|
273
304
|
if (began || cancelled)
|
|
@@ -277,11 +308,14 @@ export function createLoop(options) {
|
|
|
277
308
|
return undefined;
|
|
278
309
|
enter(current, true);
|
|
279
310
|
current.resumed = true;
|
|
311
|
+
// In the thread of the message it was answering.
|
|
312
|
+
const thread = await record.where();
|
|
313
|
+
current.took.add(thread);
|
|
280
314
|
show(await record.append({
|
|
281
315
|
role: "tool",
|
|
282
316
|
text: CUT_OFF.shown,
|
|
283
317
|
items: [{ type: "message", role: "user", content: CUT_OFF.told }],
|
|
284
|
-
}));
|
|
318
|
+
}, { thread }));
|
|
285
319
|
return { opened: undefined };
|
|
286
320
|
});
|
|
287
321
|
}),
|
|
@@ -326,7 +360,7 @@ export function createLoop(options) {
|
|
|
326
360
|
/** Streamed text of the model request running now, for the reply it becomes. */
|
|
327
361
|
delta(requestId, text) {
|
|
328
362
|
if (turn?.requestId === requestId && turn.replyId)
|
|
329
|
-
out.delta(turn.replyId, text);
|
|
363
|
+
out.delta(turn.thread ?? record.id, turn.replyId, text);
|
|
330
364
|
},
|
|
331
365
|
};
|
|
332
366
|
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { type Log, type Model, type Shown, type Status, type Store, type Tool } from "./types.js";
|
|
2
|
-
/** The turns that run at once, not counting the one more for
|
|
2
|
+
/** The turns that run at once, not counting the one more for work already begun. */
|
|
3
3
|
export declare const PLACES = 4;
|
|
4
4
|
/** The turns that may wait for a place at once. */
|
|
5
5
|
export declare const WAITING = 16;
|
|
6
|
-
/** Where a looper
|
|
6
|
+
/** Where a looper says what it does. */
|
|
7
7
|
export interface LooperOut {
|
|
8
|
+
/** A message, with the thread it is shown in. */
|
|
8
9
|
message(thread: string, message: Shown): void;
|
|
9
10
|
/** Text of the reply `id` as the model writes it. */
|
|
10
11
|
delta(thread: string, id: string, text: string): void;
|
|
11
|
-
|
|
12
|
+
/** Whether anything is at work, the main loop included: said when it changes. */
|
|
13
|
+
working(on: boolean): void;
|
|
14
|
+
/** A message in `thread` got no answer, and why. */
|
|
15
|
+
failed(thread: string, problem: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}): void;
|
|
12
19
|
}
|
|
13
20
|
/**
|
|
14
21
|
* Make a looper. Nothing is read or run until a thread is named: a message sent in it, or its
|
|
@@ -32,38 +39,41 @@ export declare function createLooper(options: {
|
|
|
32
39
|
log: Log;
|
|
33
40
|
}): {
|
|
34
41
|
/**
|
|
35
|
-
* Say a message in a thread. It is recorded and said at once.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
42
|
+
* Say a message in a thread. It is recorded and said at once. In a thread a piece of work
|
|
43
|
+
* follows it is that work's: with no turn of it running it starts one, which waits for a place
|
|
44
|
+
* if none is free, and a turn running or waiting takes it at its next model request. In a
|
|
45
|
+
* thread nothing follows, the main loop says whose it is first. Refused `busy` while `WAITING`
|
|
46
|
+
* turns already wait and it would start one more, or while the looper is stopped; and
|
|
47
|
+
* `unavailable` once it has closed.
|
|
39
48
|
*/
|
|
40
49
|
send(thread: string, text: string): Promise<{
|
|
41
50
|
messageId: string;
|
|
42
51
|
}>;
|
|
43
|
-
/** Stop a
|
|
44
|
-
cancel: (
|
|
45
|
-
/** What a
|
|
46
|
-
state: (
|
|
52
|
+
/** Stop a piece of work's turn now. A call already running finishes, and its result is kept. */
|
|
53
|
+
cancel: (work: string) => Promise<void | undefined>;
|
|
54
|
+
/** What a piece of work's loop is doing. */
|
|
55
|
+
state: (work: string) => Status;
|
|
47
56
|
/** A thread's messages before `before`, oldest first, as many as fit `limit`. */
|
|
48
57
|
page: (thread: string, before: string | undefined, limit: number) => Promise<{
|
|
49
58
|
messages: Shown[];
|
|
50
59
|
more: boolean;
|
|
51
60
|
}>;
|
|
52
61
|
/**
|
|
53
|
-
* Stop: every turn is cut, running or waiting,
|
|
54
|
-
* running finishes first, so this answers when nothing more will
|
|
55
|
-
* as they are, to be read.
|
|
62
|
+
* Stop: every turn is cut, running or waiting, the main loop is cut too, and nothing starts
|
|
63
|
+
* until `start`. A call already running finishes first, so this answers when nothing more will
|
|
64
|
+
* be written. The records stay as they are, to be read. What was cut off earlier no longer
|
|
65
|
+
* goes on, and messages waiting for a piece of work wait for their thread's next.
|
|
56
66
|
*/
|
|
57
67
|
stop(): Promise<void>;
|
|
58
68
|
/** Take messages again. The turns `stop` cut stay cut. */
|
|
59
69
|
start(): void;
|
|
60
70
|
resume: () => Promise<void>;
|
|
61
71
|
/**
|
|
62
|
-
* Close for good: every turn is cut, and none starts after. Each stays marked,
|
|
63
|
-
* in whoever opens the store next.
|
|
72
|
+
* Close for good: every turn is cut, and none starts after. Each stays marked, as does each
|
|
73
|
+
* thread where messages wait, for `resume` in whoever opens the store next.
|
|
64
74
|
*/
|
|
65
75
|
close(): Promise<void>;
|
|
66
|
-
/** Settles when every
|
|
76
|
+
/** Settles when every piece of work's latest turn has written all it will. */
|
|
67
77
|
settled(): Promise<void>;
|
|
68
78
|
/** Text the model writes while it answers `requestId`, for the reply it becomes. */
|
|
69
79
|
delta(requestId: string, text: string): void;
|