@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.
- package/README.md +3 -3
- package/dist/node_modules/@agentorg/computer/dist/types/generated/schema.d.ts +3 -0
- package/dist/node_modules/@agentorg/computer/package.json +1 -1
- package/dist/node_modules/@agentorg/computer-connector/dist/src/provider.js +3 -3
- package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.d.ts +1 -1
- package/dist/node_modules/@agentorg/computer-connector/dist/src/unit.js +34 -30
- package/dist/node_modules/@agentorg/computer-connector/package.json +1 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/index.d.ts +0 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/index.js +0 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/manager.d.ts +1 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/manager.js +1 -1
- package/dist/node_modules/@agentorg/looper/dist/src/loop.d.ts +17 -8
- package/dist/node_modules/@agentorg/looper/dist/src/loop.js +80 -41
- package/dist/node_modules/@agentorg/looper/dist/src/looper.d.ts +39 -5
- package/dist/node_modules/@agentorg/looper/dist/src/looper.js +290 -102
- package/dist/node_modules/@agentorg/looper/dist/src/main.d.ts +73 -17
- package/dist/node_modules/@agentorg/looper/dist/src/main.js +166 -77
- package/dist/node_modules/@agentorg/looper/dist/src/record.d.ts +76 -18
- package/dist/node_modules/@agentorg/looper/dist/src/record.js +190 -111
- package/dist/node_modules/@agentorg/shell/dist/app.css +255 -276
- package/dist/node_modules/@agentorg/shell/dist/app.js +144 -147
- package/dist/node_modules/assistant/dist/src/chats.d.ts +10 -5
- package/dist/node_modules/assistant/dist/src/chats.js +5 -4
- package/dist/node_modules/assistant/dist/src/engine.js +10 -4
- package/dist/node_modules/chatgpt/dist/src/account.js +1 -1
- package/dist/src/gateway.js +0 -3
- package/package.json +1 -1
- package/dist/node_modules/@agentorg/interfaces/dist/src/folders.d.ts +0 -19
- package/dist/node_modules/@agentorg/interfaces/dist/src/folders.js +0 -25
- package/dist/src/folders.d.ts +0 -2
- 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
|
|
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
|
|
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.
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15
|
-
: "On this
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
79
|
+
title: "What it reaches",
|
|
63
80
|
required: true,
|
|
64
81
|
options: [
|
|
65
|
-
{
|
|
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
|
|
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: "
|
|
75
|
-
description: "It can read and change files in its
|
|
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: "
|
|
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: [] },
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
29
|
-
let
|
|
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
|
-
/**
|
|
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.
|
|
216
|
-
*
|
|
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
|
-
|
|
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.
|
|
231
|
-
* gives nothing, no turn starts. A turn `cancelled` before it started keeps what
|
|
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
|
|
247
|
-
|
|
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.
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
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
|
|
328
|
-
*
|
|
329
|
-
*
|
|
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
|
|
341
|
-
// has written what it will
|
|
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 (
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
|
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`
|
|
46
|
-
* turns already wait and it would start one more, or while the looper is
|
|
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<
|
|
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`. */
|