@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 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. A thread has its record and its loop: a turn sends the record 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. Turns of different threads run side by side, four at once, and one more that only a thread already begun may take; a turn past that waits for a place, its message already recorded and said, and while begun threads keep five turns running a new thread goes on waiting. Sixteen turns may wait; past that a message that would start a turn is refused `busy`. A message said in a thread whose turn is running or waiting is recorded and said at once, and that turn takes it at its next model request. A thread is marked in the store while a turn of it hasn't ended: `resume` goes on, once, with each turn that was cut off by a looper closing or going without closing, and the model is told to check what is already done. Stopped, it cuts every turn 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. So an agent works in several chats 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 any chat'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 turn is running is recorded and shown at once, and that turn takes it at its next model request. A turn that fails leaves its chat with a `problem`, until that chat's next reply, and a message sent after it starts a turn that reads what went unanswered too. A turn that a restart cut off goes on once when the agent is back, its chat 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.
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 said through `out`. In a turn,
46
- * running or waiting for its place, it is recorded and said at once, and the turn takes it 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
- * Go on with a turn that was cut off before it ended, as by a restart: the thread shows one
54
- * line saying so, and the model is told once to check what is already done. Nothing goes on in
55
- * a thread with nothing kept, in one that has had a turn since the loop was made, or when a
56
- * cancel comes first.
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
- status(current, { state: "error", detail: codeOf(error) });
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 entry = await record.append({
217
- role: "user",
218
- items: [{ type: "message", role: "user", content: text }],
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 said through `out`. In a turn,
255
- * running or waiting for its place, it is recorded and said at once, and the turn takes it 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
- return said(text, true);
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, !(await record.empty()));
263
- return { opened: await said(text, false) };
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
- * Go on with a turn that was cut off before it ended, as by a restart: the thread shows one
268
- * line saying so, and the model is told once to check what is already done. Nothing goes on in
269
- * a thread with nothing kept, in one that has had a turn since the loop was made, or when a
270
- * cancel comes first.
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 a thread already begun. */
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's loops say what they do, each with the thread it is said in. */
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
- status(thread: string, status: Status): void;
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. With no turn in the thread it
36
- * starts one, which waits for a place if none is free; a turn running or waiting takes it at
37
- * its next model request. Refused `busy` while `WAITING` turns already wait and it would start
38
- * one more, or while the looper is stopped; and `unavailable` once it has closed.
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 thread's turn now. A call already running finishes, and its result is kept. */
44
- cancel: (thread: string) => Promise<void | undefined>;
45
- /** What a thread's loop is doing. */
46
- state: (thread: string) => Status;
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, and none starts until `start`. A call already
54
- * running finishes first, so this answers when nothing more will be written. The records stay
55
- * as they are, to be read. A turn that was cut off earlier no longer goes on.
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, for `resume`
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 thread's latest turn has written all it will. */
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;