@agentorg/cli 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -3
  2. package/dist/node_modules/@agentorg/computer/dist/types/generated/schema.d.ts +3 -0
  3. package/dist/node_modules/@agentorg/computer/package.json +1 -1
  4. package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.js +3 -3
  5. package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.d.ts +1 -1
  6. package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.js +34 -30
  7. package/dist/node_modules/@agentorg/computer-connector/package.json +1 -1
  8. package/dist/node_modules/@agentorg/interfaces/dist/src/index.d.ts +0 -1
  9. package/dist/node_modules/@agentorg/interfaces/dist/src/index.js +0 -1
  10. package/dist/node_modules/@agentorg/interfaces/dist/src/manager.d.ts +1 -1
  11. package/dist/node_modules/@agentorg/interfaces/dist/src/manager.js +1 -1
  12. package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +17 -8
  13. package/dist/node_modules/@agentorg/looper/dist/src/loop.js +80 -41
  14. package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +39 -5
  15. package/dist/node_modules/@agentorg/looper/dist/src/looper.js +290 -102
  16. package/dist/node_modules/@agentorg/looper/dist/src/main.d.ts +73 -17
  17. package/dist/node_modules/@agentorg/looper/dist/src/main.js +166 -77
  18. package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +76 -18
  19. package/dist/node_modules/@agentorg/looper/dist/src/record.js +190 -111
  20. package/dist/node_modules/@agentorg/shell/dist/app.css +255 -276
  21. package/dist/node_modules/@agentorg/shell/dist/app.js +144 -147
  22. package/dist/node_modules/assistant/dist/src/chats.d.ts +10 -5
  23. package/dist/node_modules/assistant/dist/src/chats.js +5 -4
  24. package/dist/node_modules/assistant/dist/src/engine.js +10 -4
  25. package/dist/node_modules/chatgpt/dist/src/account.js +1 -1
  26. package/dist/src/gateway.js +0 -3
  27. package/package.json +1 -1
  28. package/dist/node_modules/@agentorg/interfaces/dist/src/folders.d.ts +0 -19
  29. package/dist/node_modules/@agentorg/interfaces/dist/src/folders.js +0 -25
  30. package/dist/src/folders.d.ts +0 -2
  31. package/dist/src/folders.js +0 -45
package/README.md CHANGED
@@ -18,10 +18,10 @@ AgentOrg creates and manages agents. Each part is a program that joins a Hub as
18
18
  - `@agentorg/hub`: the core. Its state, in `node:sqlite` in a private home: Actors, what each takes and offers, bindings and an audit trail. An Actor is a program that connects itself, with a credential. A binding is the grant, and it needs both ends: the export side (the provider, an Actor already bound to that export, or a manager) and the slot side (the consumer itself, or a manager). Through `hub.grants@1`, a caller with both ends binds; any other is refused. `hub.grants.expose({provider, export, to, as?})` exposes an `rpc` export to one Actor: a slot made on `to` at run time, typed by the export's interface and bound to it in one step, named `as` (one word of at most 48 characters) or by default the provider's name camelCased, `_` and the export's last part, made unique on `to` with `_2`, `_3`…. Its authority is a binding's, `to` being the consumer. Exposing the same export to the same Actor again gives its slot back. The slot goes with its binding: `unexpose({to, slot})`, `unbind`, the export no longer declared or either end closed; `hub.bindings` marks such a binding `exposed: true`. A manager is any Actor bound to `hub.bindings@1`. `hub.actors.create` can bind the new Actor's slots at once, with the creator's export side. The router serves it over WebSocket (`listen`), one yamux channel per Actor: a client's first stream is `hello` with its credential, then every `call` and `subscribe` stream is resolved through its binding, validated against the interface's JSON Schemas in both directions (ajv, 2020-12, in a pool of worker threads, each check under a time budget, since schemas come from Actors), forwarded to the provider with the caller named, and audited. An Actor learns which of its slots are bound from hello's result, then from a `slot` notice on each change. Every Actor's record in `hub.actors` says whether it is `connected`, holding a channel to the Hub now, and `hub.actors`' `changed` event names an Actor whose record may differ. When a redeclaration changes the interface of a bound name, the bindings whose other end doesn't declare the new one are dropped (audited as `binding.drop`). A refusal that names an Actor names it as "Name (id)", and says what the caller can do instead.
19
19
  - `@agentorg/cli`: AgentOrg as one app. `agentorg` runs a Hub, the person's gateway and the shell on loopback; see "The app: the person's gateway".
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
- - `@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.
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 stops all of 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. 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.
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, or, when a message needs no work, the main loop. A turn of a piece of work 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 the main loop decides on the thread: one model request with one function, `route`, and no other tool. It is told the work there is, each piece by id and title, whether it is in a turn or at rest, and whether its last turn failed, and the head of what it last said (twelve at most: those in a turn, then the latest at rest), and then the thread as a conversation (its newest eight messages, the first 4,000 characters of each). It says what came in is new work, with a title, or points at work there is, which then follows that thread too and, if at rest, starts a turn; or it answers itself, in text, when the message needs no work: that text is kept and said as the thread's reply, streamed as it is written, and the thread stays no work's, so its next message is decided the same way. Text beside a call is said too, and what came in is placed all the same, unless the call names work to stop. It is told to give a message to work only when the message points at that work explicitly, and to answer only what needs no tools and never to promise work. It speaks as whoever hosts it says (`voice`), told after its own words. It decides for one thread at a time, on all that waits in the thread, and keeps nothing between decisions but what the thread holds; an answer that says and calls nothing, or places with no work it was told of, starts new work. When a turn ends, the work's record is given the time, the head of what it last said, and whether the turn failed. One piece of work is stopped by saying so. The main loop may answer a message in a thread nothing follows by stopping work in a turn that the message asks to stop, when exactly one fits: it calls `route` with `stop`, which places nothing, and what it wrote is that thread's answer, or, when it wrote nothing, `Stopped.` if a turn was cut and `Nothing was stopped.` if not. Only that thread is told, and it stays no work's. And a message said in a thread a piece of work follows, which joins a turn of that work, is, beside that turn taking it, put to the main loop with that work alone, unless that turn has ended by then: one more request, in the looper's own words without `voice`, in which it may only stop that turn. These requests take their place among the decisions, one at a time; a message that starts a turn makes none, nor does one said before the work followed its thread, which the work takes with the thread. Stopping cuts the turn the main loop was told of, as the looper's own stop does for all, and no turn that began after it: a call already running finishes and its result is kept, and the work is at rest, not failed. Where the message joined the turn, what the main loop says of stopping it is kept at once in that thread as the work's own, and the work reads it after whatever the stopped turn still writes; if the store refuses it, the turn is cut all the same and the thread is told it failed. A message said to work whose stopped turn is still writing (a running call's result, or a reply being kept) is kept and said at once, as part of that turn's tail: the turn it starts takes over the stopped turn's place and shows as running, and makes its first request once the stopped turn has written what it will, so the model reads that call's result before the message. What the main loop was to hear of is not kept: a stop, a close or a restart before it has answered drops it, and the worker has the message all the same. 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, when the main loop had it, 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 it holds messages and no work has taken them: `resume` goes on, once, with each turn and each decision 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. A thread the main loop answered, couldn't place, or was stopped over, waits for its next message, and the main loop is put what the thread holds. 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. Saying stop in a chat whose work is in a turn stops that turn, and no other work's; the agent's own Stop stops all. A message in a chat no work follows is read by the agent's main loop. It answers a plain message itself, as the agent, and that chat stays no work's; or it starts new work for it, or, when the message points at work there is, 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, then the agent's own instructions, which it speaks as, and one function: none of the agent's skills or tools. The request that only hears of a message said to work in a turn has the looper's words alone. 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
 
@@ -487,6 +487,9 @@ export interface components {
487
487
  /** @constant */
488
488
  type: "entries";
489
489
  id: string;
490
+ /** @description What the workspace is called: its folder's own name, and nothing of where it is. It comes with every entries answer, whichever folder was asked for, so that with path it names the folder. A computer in a container made before `workspace` was added sends none, and there path alone names the folder. It is `workspace` for a workspace the service made, and empty for one at the top of a disk. */
491
+ workspace?: string;
492
+ /** @description The folder as it was asked for: `.` for the workspace itself. */
490
493
  path: string;
491
494
  entries: {
492
495
  name: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentorg/computer",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -90,7 +90,7 @@ function problems(unit, values, found, secrets) {
90
90
  wrong.push({ field: "address", message: `That address won't do. ${why}` });
91
91
  }
92
92
  else if (values.service !== LOCAL)
93
- wrong.push({ field: "service", message: "Choose a service." });
93
+ wrong.push({ field: "service", message: "Choose where it resides." });
94
94
  if (values.access === "none") {
95
95
  if (values.browser !== true)
96
96
  wrong.push({
@@ -99,7 +99,7 @@ function problems(unit, values, found, secrets) {
99
99
  });
100
100
  }
101
101
  else if (!LEVELS.includes(values.access))
102
- wrong.push({ field: "access", message: "Choose what it can do." });
102
+ wrong.push({ field: "access", message: "Choose what it reaches." });
103
103
  const workspace = fromHome(values.workspace);
104
104
  if (values.service === LOCAL && workspace !== undefined) {
105
105
  let folder = false;
@@ -313,7 +313,7 @@ export async function startComputerProvider(options) {
313
313
  // The folder, so the person can find it: the service says where one it made is.
314
314
  const folder = workspace ??
315
315
  (await computer.getInfo().then((info) => info.workspace, () => null));
316
- const where = local ? "this machine" : new URL(reached.apiUrl).host;
316
+ const where = local ? "this computer" : new URL(reached.apiUrl).host;
317
317
  const on = isolation === "container" ? `${where}, in a container` : `${where}, no isolation`;
318
318
  const servers = [];
319
319
  if (access !== "none")
@@ -7,7 +7,7 @@ export type Access = "read" | "files" | "terminal";
7
7
  export declare const LOCAL = "local";
8
8
  export declare const ANOTHER = "another";
9
9
  /** What to do when there is no service here: the app never starts or ships one. */
10
- export declare const INSTALL_STEPS = "No AgentComputer is running on this machine. Install it with npm install -g @agentorg/computer, start it with agentcomputer serve, and try again.";
10
+ export declare const INSTALL_STEPS = "No AgentComputer is running on this computer. Install it with npm install -g @agentorg/computer, start it with agentcomputer serve, and try again.";
11
11
  /** How the service on this machine isolates its computers, if one is running. */
12
12
  export type Found = {
13
13
  isolation: "none" | "container";
@@ -7,12 +7,12 @@ const WITH_FILES = { slots: [{ name: COMPUTER }], skills: [skill("files"), skill
7
7
  export const LOCAL = "local";
8
8
  export const ANOTHER = "another";
9
9
  /** What to do when there is no service here: the app never starts or ships one. */
10
- export const INSTALL_STEPS = "No AgentComputer is running on this machine. Install it with npm install -g @agentorg/computer, start it with agentcomputer serve, and try again.";
10
+ export const INSTALL_STEPS = "No AgentComputer is running on this computer. Install it with npm install -g @agentorg/computer, start it with agentcomputer serve, and try again.";
11
11
  const localLine = (found) => !found
12
12
  ? INSTALL_STEPS
13
13
  : found.isolation === "container"
14
- ? "On this machine, started by you. Each computer is a container of its own."
15
- : "On this machine, started by you. No isolation: it acts as you, and with a terminal it reaches everything you can.";
14
+ ? "On this computer, by a service you started. Each agent's computer is a container of its own."
15
+ : "On this computer, by a service you started. No isolation: it acts as you, and with a terminal it reaches everything you can.";
16
16
  /** The form as it stands now, with what was found on this machine. */
17
17
  export const computerUnit = (found) => ({
18
18
  id: COMPUTER,
@@ -23,12 +23,12 @@ export const computerUnit = (found) => ({
23
23
  id: "service",
24
24
  type: "choice",
25
25
  value: "string",
26
- title: "Service",
26
+ title: "Where it resides",
27
27
  description: "Where its computer runs, and how that's kept apart from everything else.",
28
28
  // Not required: with none found, `check` says what to do, which is more than "fill this in".
29
29
  ...(found ? { default: LOCAL } : {}),
30
30
  options: [
31
- { value: LOCAL, title: "This machine", description: localLine(found) },
31
+ { value: LOCAL, title: "This computer", description: localLine(found) },
32
32
  {
33
33
  value: ANOTHER,
34
34
  title: "Another address",
@@ -41,7 +41,7 @@ export const computerUnit = (found) => ({
41
41
  type: "text",
42
42
  value: "string",
43
43
  title: "Address",
44
- description: "The service's address, over https unless it's on this machine.",
44
+ description: "The service's address, over https unless it's on this computer.",
45
45
  required: true,
46
46
  visible: { field: "service", in: [ANOTHER] },
47
47
  },
@@ -54,25 +54,46 @@ export const computerUnit = (found) => ({
54
54
  required: true,
55
55
  visible: { field: "service", in: [ANOTHER] },
56
56
  },
57
+ {
58
+ id: "workspace",
59
+ type: "directory",
60
+ value: "string",
61
+ title: "Workspace directory",
62
+ description: "The folder it works in. Left empty, it's a folder with the agent's name in ~/Agents, made if it's missing. Its file tools stay inside it by a check, not a sandbox. Removing the agent leaves it.",
63
+ placeholder: "~/Agents/{name}",
64
+ visible: { field: "service", in: [LOCAL] },
65
+ },
66
+ {
67
+ id: "path",
68
+ type: "text",
69
+ value: "string",
70
+ title: "Workspace directory",
71
+ description: "A path on the service's machine. Removing the agent leaves it.",
72
+ visible: { field: "service", in: [ANOTHER] },
73
+ },
57
74
  {
58
75
  id: "access",
59
76
  authority: "setting",
60
77
  type: "choice",
61
78
  value: "string",
62
- title: "What it can do",
79
+ title: "What it reaches",
63
80
  required: true,
64
81
  options: [
65
- { value: "none", title: "Nothing", description: "It has no files and can't run anything." },
82
+ {
83
+ value: "none",
84
+ title: "No files or terminal",
85
+ description: "It has no files and can't run anything.",
86
+ },
66
87
  {
67
88
  value: "read",
68
- title: "Read files",
69
- description: "It can read files in its working directory, but can't change them or run anything.",
89
+ title: "Read-only files",
90
+ description: "It can read files in its workspace directory, but can't change them or run anything.",
70
91
  adds: WITH_FILES,
71
92
  },
72
93
  {
73
94
  value: "files",
74
- title: "Read and change files",
75
- description: "It can read and change files in its working directory, but can't run anything.",
95
+ title: "Files",
96
+ description: "It can read and change files in its workspace directory, but can't run anything.",
76
97
  adds: WITH_FILES,
77
98
  },
78
99
  {
@@ -91,28 +112,11 @@ export const computerUnit = (found) => ({
91
112
  authority: "setting",
92
113
  type: "toggle",
93
114
  value: "boolean",
94
- title: "Browser",
115
+ title: "With a browser",
95
116
  description: "It can open web pages and act on them, in a browser of its own.",
96
117
  default: false,
97
118
  adds: { slots: [{ name: BROWSER_SLOT }], skills: [skill("browser")] },
98
119
  },
99
- {
100
- id: "workspace",
101
- type: "directory",
102
- value: "string",
103
- title: "Working directory",
104
- description: "The folder it works in. Left empty, it's a folder with the agent's name in ~/Agents, made if it's missing. Its file tools stay inside it by a check, not a sandbox. Removing the agent leaves it.",
105
- placeholder: "~/Agents/{name}",
106
- visible: { field: "service", in: [LOCAL] },
107
- },
108
- {
109
- id: "path",
110
- type: "text",
111
- value: "string",
112
- title: "Working directory",
113
- description: "A path on the service's machine. Removing the agent leaves it.",
114
- visible: { field: "service", in: [ANOTHER] },
115
- },
116
120
  ],
117
121
  needs: [],
118
122
  gives: { slots: [], skills: [], results: [] },
@@ -20,7 +20,7 @@
20
20
  "test": "node --test \"dist/test/**/*.test.js\""
21
21
  },
22
22
  "dependencies": {
23
- "@agentorg/computer": "0.0.10",
23
+ "@agentorg/computer": "0.0.11",
24
24
  "@agentorg/interfaces": "0.0.0",
25
25
  "@agentorg/mcp": "0.0.0",
26
26
  "@agentorg/sdk": "0.0.0",
@@ -3,7 +3,6 @@ export { AgentStatus, Answers, agentProvider, Creatable, CreatableId, } from "./
3
3
  export { agentSettings, SKILL_FILE_MAX, SkillEntry, SkillName, SkillPath, } from "./agent-settings.js";
4
4
  export { CHAT_TITLE_MAX, ChatEntry, ChatMessage, ChatStatus, ChatTool, chat, } from "./chat.js";
5
5
  export { ConnectionKind, ConnectionParams, ConnectionSecrets, ConnectionState, ConnectionStatus, ConnectionTarget, ConnectionTool, } from "./connection.js";
6
- export { FOLDERS_MAX, folders } from "./folders.js";
7
6
  export { Actor, ActorState, AuditEntry, Binding, ExportEntry, hubActors, hubAudit, hubBindings, hubExports, hubGrants, SlotName, } from "./hub.js";
8
7
  export { kv } from "./kv.js";
9
8
  export { Item, llmSession, OutputItem, Tool } from "./llm-session.js";
@@ -3,7 +3,6 @@ export { AgentStatus, Answers, agentProvider, Creatable, CreatableId, } from "./
3
3
  export { agentSettings, SKILL_FILE_MAX, SkillEntry, SkillName, SkillPath, } from "./agent-settings.js";
4
4
  export { CHAT_TITLE_MAX, ChatEntry, ChatMessage, ChatStatus, ChatTool, chat, } from "./chat.js";
5
5
  export { ConnectionKind, ConnectionParams, ConnectionSecrets, ConnectionState, ConnectionStatus, ConnectionTarget, ConnectionTool, } from "./connection.js";
6
- export { FOLDERS_MAX, folders } from "./folders.js";
7
6
  export { Actor, ActorState, AuditEntry, Binding, ExportEntry, hubActors, hubAudit, hubBindings, hubExports, hubGrants, SlotName, } from "./hub.js";
8
7
  export { kv } from "./kv.js";
9
8
  export { Item, llmSession, OutputItem, Tool } from "./llm-session.js";
@@ -410,7 +410,7 @@ export declare const agents: import("@agentorg/sdk").RpcInterface<"agents", {
410
410
  }, z.core.$strip>, z.ZodObject<{}, z.core.$strip>>;
411
411
  /**
412
412
  * Stops the agent: whatever it was doing ends, and it does nothing until started; all else
413
- * stays. This is the one way to stop an agent's work.
413
+ * stays.
414
414
  */
415
415
  stop: import("@agentorg/sdk").MethodSpec<z.ZodObject<{
416
416
  agent: z.ZodString;
@@ -207,7 +207,7 @@ export const agents = defineInterface({
207
207
  start: method({ input: OnAgent, output: z.object({}) }),
208
208
  /**
209
209
  * Stops the agent: whatever it was doing ends, and it does nothing until started; all else
210
- * stays. This is the one way to stop an agent's work.
210
+ * stays.
211
211
  */
212
212
  stop: method({ input: OnAgent, output: z.object({}) }),
213
213
  /** The models the agent can use, and the one chosen, as its provider says. */
@@ -50,12 +50,13 @@ export declare function createLoop(options: {
50
50
  log: Log;
51
51
  }): {
52
52
  /**
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.
53
+ * Say a message in a thread the work follows. It is recorded and said at once. In a turn,
54
+ * running or waiting for its place, the turn takes it at its next model request: `turn` is
55
+ * then that turn's number. With no turn it starts one, whose progress is said through `out`.
56
56
  */
57
57
  send: (text: string, thread: string) => Promise<{
58
58
  messageId: string;
59
+ turn?: number;
59
60
  }>;
60
61
  /**
61
62
  * Take the messages waiting in `thread`, which the work has come to follow, as its own, and
@@ -74,15 +75,23 @@ export declare function createLoop(options: {
74
75
  resumed: () => boolean;
75
76
  /** The status now. */
76
77
  state: () => Status;
78
+ /** The number of the turn running or waiting for its place, if one is. */
79
+ turn: () => number | undefined;
77
80
  /**
78
- * Stop the turn now: its model request is cancelled, no further call starts, and a new turn
79
- * may begin at once. A call already running finishes, and its result is kept. A turn waiting
80
- * for its place never starts.
81
+ * Stop the turn now, or with `only`, the turn of that number if it is still the one: its
82
+ * model request is cancelled and no further call starts. A call already running finishes,
83
+ * and its result is kept; a new turn may be started at once, and reads the record after
84
+ * that. A turn waiting for its place never starts. It says whether it cut a turn. `line` is
85
+ * what the main loop says as it stops it, kept in that thread in order with what was asked
86
+ * before.
81
87
  */
82
- cancel(): Promise<void>;
88
+ cancel(only?: number, line?: {
89
+ text: string;
90
+ thread: string;
91
+ }): Promise<boolean>;
83
92
  /** Settles when the latest turn has written all it will. */
84
93
  settled: () => Promise<void>;
85
- /** Settles when a stopped turn's running call has ended: at once, if none is running. */
94
+ /** Settles when a stopped turn has written what it will: at once, if none is writing. */
86
95
  drained: () => Promise<void>;
87
96
  /** Streamed text of the model request running now, for the reply it becomes. */
88
97
  delta(requestId: string, text: string): void;
@@ -19,14 +19,23 @@ const CUT_OFF = {
19
19
  export function createLoop(options) {
20
20
  const { model, record, out, log } = options;
21
21
  let turn;
22
- /** A cancelled turn whose running call hasn't settled: the next turn waits for its records. */
22
+ /**
23
+ * A stopped turn that had begun to run and hasn't ended: it may still write the result of its
24
+ * running call, or the reply it was keeping. What is kept meanwhile is kept as joined, so the
25
+ * model reads it after that; and the next turn reads the record once it has ended.
26
+ */
23
27
  let draining;
28
+ /**
29
+ * That turn's place, the loop's while it still writes. A turn started meanwhile asks for none:
30
+ * when the stopped turn has ended, its place goes to that turn, or is given back.
31
+ */
32
+ let lingering;
24
33
  /** Settles when the latest turn's loop has ended. */
25
34
  let lastEnded;
26
35
  /** The latest status, for a view that asks. */
27
36
  let latest = { state: "idle" };
28
- /** Set once a turn has started here: nothing that was cut off earlier is left to go on with. */
29
- let began = false;
37
+ /** The starts made here. After the first, nothing cut off earlier is left to go on with. */
38
+ let turns = 0;
30
39
  /** Counts the cancels, so that what was asked before one can tell when it is taken after it. */
31
40
  let cancels = 0;
32
41
  /** What is being taken now: the next thing asked is taken after it. */
@@ -200,9 +209,13 @@ export function createLoop(options) {
200
209
  status(current, { state: "thinking" });
201
210
  }
202
211
  }
203
- /** A turn from its place to its end: it waits for the place already shown as thinking. */
212
+ /**
213
+ * A turn from its place to its end: it waits for the place, and for what a stopped turn before
214
+ * it still writes, already shown as thinking.
215
+ */
204
216
  async function placed(current) {
205
217
  status(current, { state: "thinking" });
218
+ await draining;
206
219
  await current.place?.entered;
207
220
  current.waiting = false;
208
221
  if (current.cancelled)
@@ -212,13 +225,15 @@ export function createLoop(options) {
212
225
  await run(current);
213
226
  }
214
227
  /**
215
- * Take a turn's place, or its turn in the line for one, before anything of it is kept. A turn
216
- * cancelled already takes none: it runs nothing.
228
+ * Take a turn's place, or its turn in the line for one, before anything of it is kept. While a
229
+ * stopped turn of this work still writes, the turn takes none: it gets that turn's, so a piece
230
+ * of work holds one place. A turn cancelled already takes none: it runs nothing.
217
231
  */
218
232
  const enter = (current, begun) => {
219
233
  if (current.cancelled)
220
234
  return;
221
- current.place = options.enter(begun);
235
+ if (!lingering)
236
+ current.place = options.enter(begun);
222
237
  current.waiting = true;
223
238
  };
224
239
  const said = async (text, thread, joined) => {
@@ -227,24 +242,23 @@ export function createLoop(options) {
227
242
  return { messageId: kept.entry.id };
228
243
  };
229
244
  /**
230
- * Start a turn: `open` takes its place and keeps what opens it, then its loop runs. When `open`
231
- * gives nothing, no turn starts. A turn `cancelled` before it started keeps what opens it and
232
- * runs nothing.
245
+ * Start a turn: `open` takes its place and keeps what opens it, at once, then its loop runs.
246
+ * When `open` gives nothing, no turn starts. A turn `cancelled` before it started keeps what
247
+ * opens it and runs nothing.
233
248
  */
234
249
  const start = async (cancelled, open) => {
235
- const current = { cancelled, took: new Set() };
250
+ const current = { id: ++turns, cancelled, took: new Set() };
236
251
  if (!cancelled)
237
252
  turn = current;
238
- began = true;
239
253
  try {
240
- // A cancelled turn's last call lands in the record before anything new does.
241
- await draining;
242
- draining = undefined;
243
254
  const begun = await open(current);
244
255
  if (begun)
245
256
  begin(current);
246
- else if (turn === current)
247
- turn = undefined;
257
+ else {
258
+ current.place?.leave();
259
+ if (turn === current)
260
+ turn = undefined;
261
+ }
248
262
  return begun?.opened;
249
263
  }
250
264
  catch (error) {
@@ -256,19 +270,20 @@ export function createLoop(options) {
256
270
  };
257
271
  return {
258
272
  /**
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.
273
+ * Say a message in a thread the work follows. It is recorded and said at once. In a turn,
274
+ * running or waiting for its place, the turn takes it at its next model request: `turn` is
275
+ * then that turn's number. With no turn it starts one, whose progress is said through `out`.
262
276
  */
263
277
  send: (text, thread) => inOrder(async (cancelled) => {
264
- if (turn) {
265
- turn.took.add(thread);
266
- return said(text, thread, true);
278
+ const into = turn;
279
+ if (into) {
280
+ into.took.add(thread);
281
+ return { ...(await said(text, thread, true)), turn: into.id };
267
282
  }
268
283
  return (await start(cancelled, async (current) => {
269
284
  enter(current, await record.begun());
270
285
  current.took.add(thread);
271
- return { opened: await said(text, thread, false) };
286
+ return { opened: await said(text, thread, draining !== undefined) };
272
287
  }));
273
288
  }),
274
289
  /**
@@ -287,9 +302,8 @@ export function createLoop(options) {
287
302
  await start(false, async (current) => {
288
303
  enter(current, await record.begun());
289
304
  current.took.add(thread);
290
- if ((await record.adopt(thread, false)) > 0)
305
+ if ((await record.adopt(thread, draining !== undefined)) > 0)
291
306
  return { opened: undefined };
292
- current.place?.leave();
293
307
  return undefined;
294
308
  });
295
309
  return true;
@@ -301,7 +315,7 @@ export function createLoop(options) {
301
315
  * was made, or when a cancel comes first.
302
316
  */
303
317
  resume: () => inOrder(async (cancelled) => {
304
- if (began || cancelled)
318
+ if (turns > 0 || cancelled)
305
319
  return;
306
320
  await start(false, async (current) => {
307
321
  if ((await record.empty()) || current.cancelled)
@@ -323,37 +337,62 @@ export function createLoop(options) {
323
337
  resumed: () => turn?.resumed === true,
324
338
  /** The status now. */
325
339
  state: () => latest,
340
+ /** The number of the turn running or waiting for its place, if one is. */
341
+ turn: () => turn?.id,
326
342
  /**
327
- * Stop the turn now: its model request is cancelled, no further call starts, and a new turn
328
- * may begin at once. A call already running finishes, and its result is kept. A turn waiting
329
- * for its place never starts.
343
+ * Stop the turn now, or with `only`, the turn of that number if it is still the one: its
344
+ * model request is cancelled and no further call starts. A call already running finishes,
345
+ * and its result is kept; a new turn may be started at once, and reads the record after
346
+ * that. A turn waiting for its place never starts. It says whether it cut a turn. `line` is
347
+ * what the main loop says as it stops it, kept in that thread in order with what was asked
348
+ * before.
330
349
  */
331
- async cancel() {
350
+ async cancel(only, line) {
351
+ const current = turn;
352
+ if (only !== undefined && current?.id !== only)
353
+ return false;
332
354
  // Counted with no turn too: a message asked but not yet taken starts none after this.
333
355
  cancels++;
334
- const current = turn;
335
356
  if (!current)
336
- return;
357
+ return false;
337
358
  current.cancelled = true;
338
359
  turn = undefined;
339
360
  // A turn that will write nothing more gives its place back now: one still waiting for it,
340
- // one the model is answering, or one waiting for its end. Any other keeps its place until it
341
- // has written what it will, and what comes next waits for that: the reply it is storing,
342
- // or the result of its running call.
361
+ // one the model is answering, or one waiting for its end. Any other's place is kept until
362
+ // it has written what it will: the reply it is storing, or the result of its running call.
343
363
  const quiet = current.requestId !== undefined || current.ending === true;
344
364
  if (current.waiting || quiet)
345
365
  current.place?.leave();
346
- if (!quiet)
347
- draining = current.ended ?? draining;
366
+ else if (current.ended) {
367
+ const still = current.ended;
368
+ draining = still;
369
+ lingering = current.place;
370
+ current.place = undefined;
371
+ // Once that has ended nothing is being written by a stopped turn any more: its place
372
+ // goes to the turn that waited for this, or back.
373
+ still.finally(() => {
374
+ if (draining !== still)
375
+ return;
376
+ draining = undefined;
377
+ if (turn?.waiting && !turn.place)
378
+ turn.place = lingering;
379
+ else
380
+ lingering?.leave();
381
+ lingering = undefined;
382
+ });
383
+ }
348
384
  say({ state: "idle" });
349
- if (current.requestId)
350
- await model.cancel({ requestId: current.requestId }).catch(() => { });
385
+ await Promise.all([
386
+ current.requestId && model.cancel({ requestId: current.requestId }).catch(() => { }),
387
+ line && inOrder(async () => show(await record.stopLine(line.text, line.thread))),
388
+ ]);
389
+ return true;
351
390
  },
352
391
  /** Settles when the latest turn has written all it will. */
353
392
  settled: async () => {
354
393
  await lastEnded;
355
394
  },
356
- /** Settles when a stopped turn's running call has ended: at once, if none is running. */
395
+ /** Settles when a stopped turn has written what it will: at once, if none is writing. */
357
396
  drained: async () => {
358
397
  await draining;
359
398
  },
@@ -1,3 +1,34 @@
1
+ /**
2
+ * The looper: the loops of whoever hosts it. Messages come in on threads, which are names its host
3
+ * gives; a thread's messages are kept under `thread/<name>/` in the store. What answers them is a
4
+ * piece of work, with a loop of its own, or, when a message needs no work, the main loop.
5
+ *
6
+ * Intake is code. A message in a thread that a piece of work follows is given to that work. One in
7
+ * a thread nothing follows is kept, and the main loop decides on the thread: it answers itself, and
8
+ * the thread stays no work's; or it says whose the message is, new work, which has the thread's
9
+ * name as its id, or work there is, in a turn or at rest, which then follows that thread too. The
10
+ * main loop makes one decision at a time, and one for a thread on all that waits in it. A piece
11
+ * of work answers in the thread of the newest message it has read.
12
+ *
13
+ * One piece of work is stopped by saying so. The main loop may answer a message in a thread
14
+ * nothing follows by stopping the turn of work it was told of; and it is told of each message that
15
+ * joins a turn of the work its thread follows, beside that work getting it, to stop that turn if
16
+ * the message asks. The turn is cut and the work is at rest. What the main loop says of it is
17
+ * said where the message was: in the work's own record when it joined the turn, for the work to
18
+ * read next, and otherwise in the asking thread alone, which stays no work's.
19
+ *
20
+ * Turns run side by side, each in a place; the main loop takes none. There are `PLACES` of them,
21
+ * and one more that only work already begun (it has done something) may take, so new work never
22
+ * keeps a message from work that is under way. A turn that finds no place waits for one, in the
23
+ * order it came. While begun work keeps five turns running, new work goes on waiting. `WAITING`
24
+ * turns may wait; past that a message that would start a turn is refused `busy`.
25
+ *
26
+ * A piece of work is marked in the store while a turn of it hasn't ended, and a thread while no
27
+ * work follows it and it holds messages. A looper that closes under a turn or a decision, or goes
28
+ * without closing, leaves the mark, and `resume` on the same store goes on with it, once. A turn
29
+ * that ends, fails or is stopped leaves no mark; a thread the main loop answered, couldn't place,
30
+ * or was stopped over, waits for its next message, and the main loop is put what it holds.
31
+ */
1
32
  import { type Log, type Model, type Shown, type Status, type Store, type Tool } from "./types.js";
2
33
  /** The turns that run at once, not counting the one more for work already begun. */
3
34
  export declare const PLACES = 4;
@@ -26,12 +57,15 @@ export declare function createLooper(options: {
26
57
  model: Model;
27
58
  /**
28
59
  * Read for each model request, since any of it may change between two: what the model is told,
29
- * what it can call, and which model to ask, none meaning the model's own default.
60
+ * what it can call, and which model to ask, none meaning the model's own default. `voice` is
61
+ * who the main loop speaks as when it answers a message itself: what the host's own model is
62
+ * told of itself, with nothing of what it can call.
30
63
  */
31
64
  request(): Promise<{
32
65
  instructions: string;
33
66
  tools: Tool[];
34
67
  model?: string | undefined;
68
+ voice?: string | undefined;
35
69
  }>;
36
70
  out: LooperOut;
37
71
  /** Whether it starts stopped: no turn starts until `start`. */
@@ -42,15 +76,15 @@ export declare function createLooper(options: {
42
76
  * Say a message in a thread. It is recorded and said at once. In a thread a piece of work
43
77
  * follows it is that work's: with no turn of it running it starts one, which waits for a place
44
78
  * 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.
79
+ * thread nothing follows, the main loop answers it or says whose it is first. Refused `busy`
80
+ * while `WAITING` turns already wait and it would start one more, or while the looper is
81
+ * stopped; and `unavailable` once it has closed.
48
82
  */
49
83
  send(thread: string, text: string): Promise<{
50
84
  messageId: string;
51
85
  }>;
52
86
  /** 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>;
87
+ cancel: (work: string) => Promise<undefined>;
54
88
  /** What a piece of work's loop is doing. */
55
89
  state: (work: string) => Status;
56
90
  /** A thread's messages before `before`, oldest first, as many as fit `limit`. */