@coltonkirsten/a12s 0.2.1 → 0.4.0
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/dist/a12s.js +491 -19
- package/dist/src/client.d.ts +20 -1
- package/dist/src/client.js +231 -19
- package/dist/src/fleet.d.ts +160 -0
- package/dist/src/fleet.js +306 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +4 -1
- package/dist/src/node.d.ts +8 -0
- package/dist/src/node.js +15 -2
- package/dist/src/types.d.ts +180 -12
- package/package.json +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Runs in Node, Deno, Bun and browsers: no node: imports here (`npm run check:browser` enforces it).
|
|
2
|
+
import { A12sClient, A12sError, TurnStream, } from "./client.js";
|
|
3
|
+
const enc = encodeURIComponent;
|
|
4
|
+
const maxReplaceTries = 5;
|
|
5
|
+
/**
|
|
6
|
+
* A running Fleet. Its agent computers, Services and tokens are in {@link agentComputers}, {@link services} and
|
|
7
|
+
* {@link tokens}.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const fleet = await Fleet.connect();
|
|
11
|
+
* const hello = await fleet.agentComputers.create("hello", { instructions: "Be helpful" });
|
|
12
|
+
* await hello.prompt("hi");
|
|
13
|
+
* await hello.remove();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class Fleet {
|
|
17
|
+
/** The lower-level client the handles use, for API calls that have no method here. */
|
|
18
|
+
client;
|
|
19
|
+
agentComputers;
|
|
20
|
+
services;
|
|
21
|
+
tokens;
|
|
22
|
+
credentials;
|
|
23
|
+
/** No discovery: `url` and `token` as given, by default the page's own origin. */
|
|
24
|
+
constructor(opts = {}) {
|
|
25
|
+
this.client = new A12sClient(opts);
|
|
26
|
+
this.agentComputers = new AgentComputers(this.client);
|
|
27
|
+
this.services = new Services(this.client);
|
|
28
|
+
this.tokens = new Tokens(this.client);
|
|
29
|
+
this.credentials = new Credentials(this.client);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* In a browser: the Fleet behind the page's own origin, which is `a12s gateway` serving the page (no token
|
|
33
|
+
* needed). In Node: found like {@link connect} finds it. Pass `url` and `token` to reach anything else.
|
|
34
|
+
*/
|
|
35
|
+
static async connect(opts = {}) {
|
|
36
|
+
return new Fleet(opts);
|
|
37
|
+
}
|
|
38
|
+
get url() {
|
|
39
|
+
return this.client.url;
|
|
40
|
+
}
|
|
41
|
+
async get() {
|
|
42
|
+
const r = await this.client.request("GET", "/v1/fleet");
|
|
43
|
+
return { version: r.version, spec: r.fleet ?? emptySpec(), drift: r.drift ?? [] };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the Fleet, lets `fn` change it (in place, or by returning a new spec) and writes it back only if nobody
|
|
47
|
+
* wrote in between; otherwise it reads again and retries, up to 5 times. `spec.defaults` is always present.
|
|
48
|
+
*/
|
|
49
|
+
async replace(fn) {
|
|
50
|
+
for (let attempt = 1;; attempt++) {
|
|
51
|
+
const cur = await this.get();
|
|
52
|
+
cur.spec.defaults ??= {};
|
|
53
|
+
const next = (await fn(cur.spec)) ?? cur.spec;
|
|
54
|
+
try {
|
|
55
|
+
await this.client.request("PUT", "/v1/fleet", { fleet: next }, {
|
|
56
|
+
headers: { "if-match": `"${cur.version}"` },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (attempt < maxReplaceTries && err instanceof A12sError && err.code === "version_mismatch")
|
|
61
|
+
continue;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return this.get();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Live events of the named agent computers, or (no argument) all of them, and the Fleet's own (`fleet.changed`,
|
|
69
|
+
* `agent_computer.phase`). Every stream of this Fleet shares one connection, which reconnects without losing
|
|
70
|
+
* events. `await stream.ready()` before sending work you want to see from the start; `close()` (or `break`) ends it.
|
|
71
|
+
* `turn` or `after` (one agent computer) replays from its journal instead.
|
|
72
|
+
*/
|
|
73
|
+
events(agentComputers, opts = {}) {
|
|
74
|
+
if (opts.turn !== undefined || opts.after !== undefined)
|
|
75
|
+
return this.client.events(agentComputers, opts);
|
|
76
|
+
return this.client.sharedEvents(namesOf(agentComputers), opts);
|
|
77
|
+
}
|
|
78
|
+
/** Finished Turns of the named agent computers, or all of them, as they end. */
|
|
79
|
+
turns(agentComputers, opts = {}) {
|
|
80
|
+
return new TurnStream(this.client.sharedEvents(namesOf(agentComputers), opts));
|
|
81
|
+
}
|
|
82
|
+
/** Every agent computer and Service: phase, session, active Turn, inbox, power. */
|
|
83
|
+
status() {
|
|
84
|
+
return this.client.status();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function namesOf(names) {
|
|
88
|
+
return typeof names === "string" ? [names] : names;
|
|
89
|
+
}
|
|
90
|
+
function emptySpec() {
|
|
91
|
+
return { apiVersion: "a12s.ai/v1alpha1", kind: "Fleet", metadata: { name: "" }, agent_computers: {} };
|
|
92
|
+
}
|
|
93
|
+
function writeHeaders(opts) {
|
|
94
|
+
return opts.replace ? {} : { "if-none-match": "*" };
|
|
95
|
+
}
|
|
96
|
+
export class AgentComputers {
|
|
97
|
+
client;
|
|
98
|
+
constructor(client) {
|
|
99
|
+
this.client = client;
|
|
100
|
+
}
|
|
101
|
+
/** Adds an agent computer to the Fleet and starts it. Fails with code `exists` if the name is taken. */
|
|
102
|
+
async create(name, spec = {}) {
|
|
103
|
+
const { replace, ...body } = spec;
|
|
104
|
+
const r = await this.client.request("PUT", `/v1/agent-computers/${enc(name)}`, body, {
|
|
105
|
+
headers: writeHeaders({ replace }),
|
|
106
|
+
});
|
|
107
|
+
return r.agentComputer ? new AgentComputer(this.client, r.agentComputer) : this.get(name);
|
|
108
|
+
}
|
|
109
|
+
/** Throws {@link NotFound} if there is none. */
|
|
110
|
+
async get(name) {
|
|
111
|
+
return new AgentComputer(this.client, await this.client.request("GET", `/v1/agent-computers/${enc(name)}`));
|
|
112
|
+
}
|
|
113
|
+
async list() {
|
|
114
|
+
const r = await this.client.request("GET", "/v1/agent-computers");
|
|
115
|
+
return (r.agentComputers ?? []).map((d) => new AgentComputer(this.client, d));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** A handle on one agent computer. `spec`, `effective` and `status` are as of the last create, get, reload or update. */
|
|
119
|
+
export class AgentComputer {
|
|
120
|
+
client;
|
|
121
|
+
name;
|
|
122
|
+
spec;
|
|
123
|
+
/** The spec with the Fleet's defaults merged in. */
|
|
124
|
+
effective;
|
|
125
|
+
status;
|
|
126
|
+
constructor(client, data) {
|
|
127
|
+
this.client = client;
|
|
128
|
+
this.name = data.name;
|
|
129
|
+
this.set(data);
|
|
130
|
+
}
|
|
131
|
+
set(d) {
|
|
132
|
+
this.spec = d.spec;
|
|
133
|
+
this.effective = d.effective;
|
|
134
|
+
this.status = d.status;
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
get path() {
|
|
138
|
+
return `/v1/agent-computers/${enc(this.name)}`;
|
|
139
|
+
}
|
|
140
|
+
async reload() {
|
|
141
|
+
return this.set(await this.client.request("GET", this.path));
|
|
142
|
+
}
|
|
143
|
+
/** Changes the spec: a key set to null goes back to the Fleet's default. A busy one picks it up when idle. */
|
|
144
|
+
async update(changes) {
|
|
145
|
+
const r = await this.client.request("PATCH", this.path, changes);
|
|
146
|
+
return r.agentComputer ? this.set(r.agentComputer) : this.reload();
|
|
147
|
+
}
|
|
148
|
+
/** Removes it from the Fleet and stops it; its drives are kept. */
|
|
149
|
+
async remove() {
|
|
150
|
+
await this.client.request("DELETE", this.path);
|
|
151
|
+
}
|
|
152
|
+
/** Sends a Message, waits for the Turn that delivers it and resolves when that Turn ends. */
|
|
153
|
+
prompt(text, opts = {}) {
|
|
154
|
+
return this.client.prompt(this.name, text, opts);
|
|
155
|
+
}
|
|
156
|
+
/** Puts a Message in the inbox and returns at once; it is delivered at the next idle moment. */
|
|
157
|
+
send(text, opts = {}) {
|
|
158
|
+
return this.client.send(this.name, { ...opts, body: text });
|
|
159
|
+
}
|
|
160
|
+
/** Its events, live, from the Fleet's shared connection; `turn` replays one Turn from its start instead. */
|
|
161
|
+
events(opts = {}) {
|
|
162
|
+
if (opts.turn !== undefined || opts.after !== undefined)
|
|
163
|
+
return this.client.events(this.name, opts);
|
|
164
|
+
return this.client.sharedEvents([this.name], opts);
|
|
165
|
+
}
|
|
166
|
+
turns(opts = {}) {
|
|
167
|
+
const { limit, ...rest } = opts;
|
|
168
|
+
if (limit !== undefined)
|
|
169
|
+
return this.pastTurns(limit);
|
|
170
|
+
return new TurnStream(this.client.sharedEvents([this.name], rest));
|
|
171
|
+
}
|
|
172
|
+
async pastTurns(limit) {
|
|
173
|
+
const r = await this.client.request("GET", `${this.path}/turns?limit=${limit}`);
|
|
174
|
+
return (r.turns ?? []).map((t) => ({
|
|
175
|
+
...t,
|
|
176
|
+
computer: t.agentComputer,
|
|
177
|
+
tools: t.tools.map((tc) => ({ ...tc, isError: tc.isError ?? false })),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
/** Stops its processes and keeps its disks, session and inbox. */
|
|
181
|
+
sleep() {
|
|
182
|
+
return this.client.sleep(this.name);
|
|
183
|
+
}
|
|
184
|
+
wake() {
|
|
185
|
+
return this.client.wake(this.name);
|
|
186
|
+
}
|
|
187
|
+
/** Interrupts a Turn (default: the active one). */
|
|
188
|
+
stop(turn) {
|
|
189
|
+
return this.client.stop(this.name, turn);
|
|
190
|
+
}
|
|
191
|
+
inbox(opts = {}) {
|
|
192
|
+
return this.client.inbox(this.name, opts);
|
|
193
|
+
}
|
|
194
|
+
message(id) {
|
|
195
|
+
return this.client.message(this.name, id);
|
|
196
|
+
}
|
|
197
|
+
/** Withdraws a pending Message; a 409 A12sError names its state if it is too late. */
|
|
198
|
+
withdraw(id) {
|
|
199
|
+
return this.client.withdraw(this.name, id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export class Services {
|
|
203
|
+
client;
|
|
204
|
+
constructor(client) {
|
|
205
|
+
this.client = client;
|
|
206
|
+
}
|
|
207
|
+
/** Adds a Service to the Fleet. Fails with code `exists` if the name is taken. */
|
|
208
|
+
async create(name, spec = {}) {
|
|
209
|
+
const { replace, ...body } = spec;
|
|
210
|
+
const r = await this.client.request("PUT", `/v1/services/${enc(name)}`, body, {
|
|
211
|
+
headers: writeHeaders({ replace }),
|
|
212
|
+
});
|
|
213
|
+
return r.service ? new Service(this.client, r.service) : this.get(name);
|
|
214
|
+
}
|
|
215
|
+
/** Throws {@link NotFound} if there is none. */
|
|
216
|
+
async get(name) {
|
|
217
|
+
return new Service(this.client, await this.client.service(name));
|
|
218
|
+
}
|
|
219
|
+
async list() {
|
|
220
|
+
return (await this.client.services()).map((s) => new Service(this.client, s));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** A handle on one Service. `spec` and `status` are as of the last create, get, reload, update or power change. */
|
|
224
|
+
export class Service {
|
|
225
|
+
client;
|
|
226
|
+
name;
|
|
227
|
+
status;
|
|
228
|
+
constructor(client, status) {
|
|
229
|
+
this.client = client;
|
|
230
|
+
this.name = status.name;
|
|
231
|
+
this.status = status;
|
|
232
|
+
}
|
|
233
|
+
/** As written in the Fleet. */
|
|
234
|
+
get spec() {
|
|
235
|
+
return this.status.spec ?? {};
|
|
236
|
+
}
|
|
237
|
+
get path() {
|
|
238
|
+
return `/v1/services/${enc(this.name)}`;
|
|
239
|
+
}
|
|
240
|
+
async reload() {
|
|
241
|
+
this.status = await this.client.service(this.name);
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
/** Changes the spec as a JSON merge patch: a key set to null is removed. */
|
|
245
|
+
async update(changes) {
|
|
246
|
+
const r = await this.client.request("PATCH", this.path, changes);
|
|
247
|
+
if (!r.service)
|
|
248
|
+
return this.reload();
|
|
249
|
+
this.status = r.service;
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
/** Removes it from the Fleet and stops it; its state drive is kept. */
|
|
253
|
+
async remove() {
|
|
254
|
+
await this.client.request("DELETE", this.path);
|
|
255
|
+
}
|
|
256
|
+
sleep() {
|
|
257
|
+
return this.power("asleep");
|
|
258
|
+
}
|
|
259
|
+
wake() {
|
|
260
|
+
return this.power("awake");
|
|
261
|
+
}
|
|
262
|
+
async power(state) {
|
|
263
|
+
this.status = await this.client.request("POST", `${this.path}/power`, { state });
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export class Tokens {
|
|
268
|
+
client;
|
|
269
|
+
constructor(client) {
|
|
270
|
+
this.client = client;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A token for another program; owner only. The secret is in the result once. With `prefix` it may do everything
|
|
274
|
+
* the owner can for names that start with it, and nothing outside; otherwise `actions` on `agentComputers`.
|
|
275
|
+
*/
|
|
276
|
+
create(name, opts) {
|
|
277
|
+
if ("prefix" in opts) {
|
|
278
|
+
return this.client.createToken({ name, prefix: opts.prefix });
|
|
279
|
+
}
|
|
280
|
+
return this.client.createToken({ name, actions: opts.actions, computers: opts.agentComputers ?? ["*"] });
|
|
281
|
+
}
|
|
282
|
+
async list() {
|
|
283
|
+
return (await this.client.request("GET", "/v1/tokens")).tokens ?? [];
|
|
284
|
+
}
|
|
285
|
+
async remove(name) {
|
|
286
|
+
await this.client.request("DELETE", `/v1/tokens/${enc(name)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Credentials by name: set once, then name them in a spec's `credentials`. Values are never read back. */
|
|
290
|
+
export class Credentials {
|
|
291
|
+
client;
|
|
292
|
+
constructor(client) {
|
|
293
|
+
this.client = client;
|
|
294
|
+
}
|
|
295
|
+
/** Stores the value on the Controller; agent computers that use it get the new value at once. */
|
|
296
|
+
set(name, value) {
|
|
297
|
+
return this.client.request("PUT", `/v1/credentials/${enc(name)}`, { value });
|
|
298
|
+
}
|
|
299
|
+
async list() {
|
|
300
|
+
return (await this.client.request("GET", "/v1/credentials")).credentials ?? [];
|
|
301
|
+
}
|
|
302
|
+
/** Fails with A12sError code `in_use` while an agent computer or Service uses it. */
|
|
303
|
+
async remove(name) {
|
|
304
|
+
await this.client.request("DELETE", `/v1/credentials/${enc(name)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { A12sClient, type A12sClientOptions } from "./client.js";
|
|
2
2
|
export * from "./client.js";
|
|
3
|
+
export * from "./fleet.js";
|
|
3
4
|
export * from "./types.js";
|
|
4
5
|
export type ConnectOptions = A12sClientOptions;
|
|
5
6
|
/**
|
|
7
|
+
* @deprecated Use `Fleet.connect()`. Kept for one release.
|
|
8
|
+
*
|
|
6
9
|
* In a browser: a client for the page's own origin, which is `a12s gateway` serving the page (no token needed).
|
|
7
10
|
* Pass `url` and `token` to reach anything else.
|
|
8
11
|
*/
|
package/dist/src/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// The default entry (browsers, Deno, workers): no node: imports. Node resolves "@coltonkirsten/a12s" to ./node.ts instead, through
|
|
2
|
-
// the "node" export condition, so `import {
|
|
2
|
+
// the "node" export condition, so `import { Fleet } from "@coltonkirsten/a12s"` works everywhere.
|
|
3
3
|
import { A12sClient } from "./client.js";
|
|
4
4
|
export * from "./client.js";
|
|
5
|
+
export * from "./fleet.js";
|
|
5
6
|
export * from "./types.js";
|
|
6
7
|
/**
|
|
8
|
+
* @deprecated Use `Fleet.connect()`. Kept for one release.
|
|
9
|
+
*
|
|
7
10
|
* In a browser: a client for the page's own origin, which is `a12s gateway` serving the page (no token needed).
|
|
8
11
|
* Pass `url` and `token` to reach anything else.
|
|
9
12
|
*/
|
package/dist/src/node.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { A12sClient, type A12sClientOptions } from "./client.js";
|
|
2
|
+
import { Fleet as PageFleet } from "./fleet.js";
|
|
2
3
|
export * from "./client.js";
|
|
4
|
+
export * from "./fleet.js";
|
|
3
5
|
export * from "./types.js";
|
|
6
|
+
export declare class Fleet extends PageFleet {
|
|
7
|
+
/** A running Fleet, as its owner, found like {@link connect} finds it. */
|
|
8
|
+
static connect(opts?: ConnectOptions): Promise<Fleet>;
|
|
9
|
+
}
|
|
4
10
|
export interface ConnectOptions extends A12sClientOptions {
|
|
5
11
|
/** The Fleet's name (`metadata.name`). Default: found as described on {@link connect}. */
|
|
6
12
|
fleet?: string;
|
|
@@ -10,6 +16,8 @@ export interface ConnectOptions extends A12sClientOptions {
|
|
|
10
16
|
cwd?: string;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
19
|
+
* @deprecated Use `Fleet.connect()`, which finds the Fleet the same way. Kept for one release.
|
|
20
|
+
*
|
|
13
21
|
* A client for a running Fleet, as its owner. It uses the first of:
|
|
14
22
|
* 1. `url` (and `token`), or `$A12S_URL` (and `$A12S_TOKEN`);
|
|
15
23
|
* 2. the Fleet named by `fleet`;
|
package/dist/src/node.js
CHANGED
|
@@ -3,9 +3,19 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { A12sClient } from "./client.js";
|
|
6
|
+
import { Fleet as PageFleet } from "./fleet.js";
|
|
6
7
|
export * from "./client.js";
|
|
8
|
+
export * from "./fleet.js";
|
|
7
9
|
export * from "./types.js";
|
|
10
|
+
export class Fleet extends PageFleet {
|
|
11
|
+
/** A running Fleet, as its owner, found like {@link connect} finds it. */
|
|
12
|
+
static async connect(opts = {}) {
|
|
13
|
+
return new Fleet(await discover(opts));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
8
16
|
/**
|
|
17
|
+
* @deprecated Use `Fleet.connect()`, which finds the Fleet the same way. Kept for one release.
|
|
18
|
+
*
|
|
9
19
|
* A client for a running Fleet, as its owner. It uses the first of:
|
|
10
20
|
* 1. `url` (and `token`), or `$A12S_URL` (and `$A12S_TOKEN`);
|
|
11
21
|
* 2. the Fleet named by `fleet`;
|
|
@@ -14,9 +24,12 @@ export * from "./types.js";
|
|
|
14
24
|
* For 2–4 it reads `~/.a12s/<fleet>/controller.json`, which `a12s up` writes.
|
|
15
25
|
*/
|
|
16
26
|
export async function connect(opts = {}) {
|
|
27
|
+
return new A12sClient(await discover(opts));
|
|
28
|
+
}
|
|
29
|
+
async function discover(opts) {
|
|
17
30
|
const url = opts.url ?? process.env.A12S_URL;
|
|
18
31
|
if (url)
|
|
19
|
-
return
|
|
32
|
+
return { url, token: opts.token ?? process.env.A12S_TOKEN, fetch: opts.fetch };
|
|
20
33
|
const home = join(opts.home ?? homedir(), ".a12s");
|
|
21
34
|
const fleet = opts.fleet ?? (await fleetNameNearby(opts.cwd ?? process.cwd())) ?? (await onlyRunningFleet(home));
|
|
22
35
|
const path = join(home, fleet, "controller.json");
|
|
@@ -27,7 +40,7 @@ export async function connect(opts = {}) {
|
|
|
27
40
|
catch (err) {
|
|
28
41
|
throw new Error(`Fleet ${fleet} isn't running (no ${path}): run a12s up in its directory first`, { cause: err });
|
|
29
42
|
}
|
|
30
|
-
return
|
|
43
|
+
return { url: `http://127.0.0.1:${cf.port}`, token: cf.token, fetch: opts.fetch };
|
|
31
44
|
}
|
|
32
45
|
async function fleetNameNearby(cwd) {
|
|
33
46
|
const file = process.env.A12S_FLEET ? resolve(cwd, process.env.A12S_FLEET) : await findUp(cwd, "fleet.yaml");
|