@coltonkirsten/a12s 0.0.0-stage → 0.2.1
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 +517 -0
- package/dist/src/client.d.ts +147 -0
- package/dist/src/client.js +520 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +12 -0
- package/dist/src/node.d.ts +22 -0
- package/dist/src/node.js +84 -0
- package/dist/src/sse.d.ts +7 -0
- package/dist/src/sse.js +45 -0
- package/dist/src/types.d.ts +377 -0
- package/dist/src/types.js +2 -0
- package/package.json +55 -4
- package/README.md +0 -3
package/dist/a12s.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
// src/sse.ts
|
|
2
|
+
async function* readSSE(body) {
|
|
3
|
+
const decoder = new TextDecoder();
|
|
4
|
+
let buf = "";
|
|
5
|
+
let frame = { data: [] };
|
|
6
|
+
const reader = body.getReader();
|
|
7
|
+
try {
|
|
8
|
+
for (; ; ) {
|
|
9
|
+
const { value, done } = await reader.read();
|
|
10
|
+
if (done) return;
|
|
11
|
+
buf += decoder.decode(value, { stream: true });
|
|
12
|
+
let nl;
|
|
13
|
+
while ((nl = buf.search(/\r\n|\r|\n/)) >= 0) {
|
|
14
|
+
if (buf[nl] === "\r" && nl === buf.length - 1) break;
|
|
15
|
+
const line = buf.slice(0, nl);
|
|
16
|
+
buf = buf.slice(nl + (buf.startsWith("\r\n", nl) ? 2 : 1));
|
|
17
|
+
if (line === "") {
|
|
18
|
+
if (frame.data.length > 0 || frame.event) {
|
|
19
|
+
yield { id: frame.id, event: frame.event, data: frame.data.join("\n") };
|
|
20
|
+
}
|
|
21
|
+
frame = { data: [] };
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (line.startsWith(":")) continue;
|
|
25
|
+
const colon = line.indexOf(":");
|
|
26
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
27
|
+
let val = colon < 0 ? "" : line.slice(colon + 1);
|
|
28
|
+
if (val.startsWith(" ")) val = val.slice(1);
|
|
29
|
+
if (field === "data") frame.data.push(val);
|
|
30
|
+
else if (field === "id") frame.id = val;
|
|
31
|
+
else if (field === "event") frame.event = val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} finally {
|
|
35
|
+
reader.releaseLock();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/client.ts
|
|
40
|
+
var A12sError = class extends Error {
|
|
41
|
+
status;
|
|
42
|
+
/** A stable code such as `busy`, `asleep`, `no_grant`, `inbox_full`, `not_found` (see docs/api.md#errors). */
|
|
43
|
+
code;
|
|
44
|
+
fix;
|
|
45
|
+
details;
|
|
46
|
+
constructor(status, body) {
|
|
47
|
+
super(body.fix ? `${body.code}: ${body.message} fix: ${body.fix}` : `${body.code}: ${body.message}`);
|
|
48
|
+
this.name = "A12sError";
|
|
49
|
+
this.status = status;
|
|
50
|
+
this.code = body.code;
|
|
51
|
+
this.fix = body.fix;
|
|
52
|
+
this.details = body.details;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var A12sGapError = class extends Error {
|
|
56
|
+
after;
|
|
57
|
+
data;
|
|
58
|
+
constructor(data) {
|
|
59
|
+
super(`journal gap after seq ${data.after ?? "(none)"}: events were lost; resync with status()`);
|
|
60
|
+
this.name = "A12sGapError";
|
|
61
|
+
this.after = data.after;
|
|
62
|
+
this.data = data;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var A12sStreamError = class extends Error {
|
|
66
|
+
constructor(message, options) {
|
|
67
|
+
super(message, options);
|
|
68
|
+
this.name = "A12sStreamError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var enc = encodeURIComponent;
|
|
72
|
+
var A12sClient = class {
|
|
73
|
+
url;
|
|
74
|
+
token;
|
|
75
|
+
fetchImpl;
|
|
76
|
+
constructor(opts = {}) {
|
|
77
|
+
this.url = (opts.url ?? "").replace(/\/+$/, "");
|
|
78
|
+
this.token = opts.token;
|
|
79
|
+
this.fetchImpl = opts.fetch ?? ((...args) => fetch(...args));
|
|
80
|
+
}
|
|
81
|
+
headers(extra = {}) {
|
|
82
|
+
return this.token ? { authorization: `Bearer ${this.token}`, ...extra } : extra;
|
|
83
|
+
}
|
|
84
|
+
async request(method, path, body, signal) {
|
|
85
|
+
const res = await this.fetchImpl(this.url + path, {
|
|
86
|
+
method,
|
|
87
|
+
headers: this.headers(body === void 0 ? {} : { "content-type": "application/json" }),
|
|
88
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
89
|
+
signal
|
|
90
|
+
});
|
|
91
|
+
const text = await res.text();
|
|
92
|
+
if (!res.ok) throw new A12sError(res.status, parseError(res.status, text));
|
|
93
|
+
return text ? JSON.parse(text) : void 0;
|
|
94
|
+
}
|
|
95
|
+
/** Every Computer and Service: phase, session, active Turn, inbox, power. */
|
|
96
|
+
status() {
|
|
97
|
+
return this.request("GET", "/v1/status");
|
|
98
|
+
}
|
|
99
|
+
async computer(name) {
|
|
100
|
+
const c = (await this.status()).computers.find((x) => x.name === name);
|
|
101
|
+
if (!c) throw new A12sError(404, { code: "not_found", message: `no Computer ${name}` });
|
|
102
|
+
return c;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Puts a Message in a Computer's inbox and returns at once. It is delivered at the Computer's next idle moment,
|
|
106
|
+
* batched with anything else pending. Works while the Computer is busy or asleep.
|
|
107
|
+
*/
|
|
108
|
+
send(computer, msg) {
|
|
109
|
+
const m = typeof msg === "string" ? { body: msg } : { ...msg };
|
|
110
|
+
m.idempotencyKey ??= globalThis.crypto.randomUUID();
|
|
111
|
+
return this.request("POST", `/v1/computers/${enc(computer)}/inbox`, m);
|
|
112
|
+
}
|
|
113
|
+
/** Sends a Message, waits for the Turn that delivers it and resolves when that Turn ends: what `a12s prompt` does. */
|
|
114
|
+
async prompt(computer, body, opts = {}) {
|
|
115
|
+
const { signal, onEvent, ...msg } = opts;
|
|
116
|
+
const message = await this.send(computer, { ...msg, body });
|
|
117
|
+
const turn = await this.deliveredIn(computer, message.id, signal);
|
|
118
|
+
const builder = new TurnBuilder();
|
|
119
|
+
for await (const ev of this.events(computer, { turn, signal, onGap: "throw" })) {
|
|
120
|
+
onEvent?.(ev);
|
|
121
|
+
const done = builder.add(ev);
|
|
122
|
+
if (done && done.id === turn) return { ...done, message };
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`event stream for turn ${turn} ended early`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Live events of one Computer, several, or (no argument) every Computer in the Fleet, merged. Each event carries
|
|
128
|
+
* its `computer`. Reconnects after a network drop without losing or repeating events. Call `await stream.ready()`
|
|
129
|
+
* before sending work you want to see from the start; `close()` (or `break`) ends it.
|
|
130
|
+
*/
|
|
131
|
+
events(computers, opts = {}) {
|
|
132
|
+
const names = typeof computers === "string" ? [computers] : computers;
|
|
133
|
+
if ((opts.turn !== void 0 || opts.after !== void 0) && names?.length !== 1) {
|
|
134
|
+
throw new TypeError("events: turn and after need exactly one Computer");
|
|
135
|
+
}
|
|
136
|
+
return new EventStream((name, live, signal) => {
|
|
137
|
+
const q = opts.turn ? `?turn=${enc(opts.turn)}` : "";
|
|
138
|
+
return this.stream(name, `/v1/computers/${enc(name)}/events${q}`, opts.after, { ...opts, signal, live });
|
|
139
|
+
}, names ?? (async () => (await this.status()).computers.map((c) => c.name)), opts.signal);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Finished Turns of one Computer, several or all, as they end: who sent the work (`messages`), what the agent said
|
|
143
|
+
* (`text`) and how it ended (`status`). Same `ready()` and `close()` as {@link events}.
|
|
144
|
+
*/
|
|
145
|
+
turns(computers, opts = {}) {
|
|
146
|
+
return new TurnStream(this.events(computers, opts));
|
|
147
|
+
}
|
|
148
|
+
inbox(computer, opts = {}) {
|
|
149
|
+
const q = new URLSearchParams();
|
|
150
|
+
if (opts.state) q.set("state", opts.state);
|
|
151
|
+
if (opts.bodies) q.set("bodies", "true");
|
|
152
|
+
if (opts.after !== void 0) q.set("after", String(opts.after));
|
|
153
|
+
const qs = q.toString();
|
|
154
|
+
return this.request("GET", `/v1/computers/${enc(computer)}/inbox${qs ? "?" + qs : ""}`);
|
|
155
|
+
}
|
|
156
|
+
message(computer, id) {
|
|
157
|
+
return this.request("GET", `/v1/computers/${enc(computer)}/inbox/${enc(id)}`);
|
|
158
|
+
}
|
|
159
|
+
/** Withdraws a pending Message; a 409 A12sError names its state if it is too late. */
|
|
160
|
+
withdraw(computer, id) {
|
|
161
|
+
return this.request("DELETE", `/v1/computers/${enc(computer)}/inbox/${enc(id)}`);
|
|
162
|
+
}
|
|
163
|
+
/** Interrupts a Turn (default: the active one). The Turn ends `interrupted`; nothing is replayed. */
|
|
164
|
+
async stop(computer, turn) {
|
|
165
|
+
const id = turn ?? (await this.computer(computer)).activeTurn;
|
|
166
|
+
if (id) await this.request("POST", `/v1/computers/${enc(computer)}/turns/${enc(id)}/stop`, {});
|
|
167
|
+
}
|
|
168
|
+
/** Stops the Computer's processes and keeps its disks, session and inbox. */
|
|
169
|
+
async sleep(computer) {
|
|
170
|
+
await this.request("POST", `/v1/computers/${enc(computer)}/power`, { state: "asleep" });
|
|
171
|
+
}
|
|
172
|
+
/** Starts a sleeping Computer; its session resumes on the next Turn. */
|
|
173
|
+
async wake(computer) {
|
|
174
|
+
await this.request("POST", `/v1/computers/${enc(computer)}/power`, { state: "awake" });
|
|
175
|
+
}
|
|
176
|
+
async services() {
|
|
177
|
+
return (await this.request("GET", "/v1/services")).services ?? [];
|
|
178
|
+
}
|
|
179
|
+
service(name) {
|
|
180
|
+
return this.request("GET", `/v1/services/${enc(name)}`);
|
|
181
|
+
}
|
|
182
|
+
/** A scoped token for another program; owner only. The secret is in the response once. */
|
|
183
|
+
createToken(req) {
|
|
184
|
+
return this.request("POST", "/v1/tokens", req);
|
|
185
|
+
}
|
|
186
|
+
/** What `a12s up` sends: validate and apply a whole Fleet. Most apps leave this to the CLI. */
|
|
187
|
+
putFleet(req) {
|
|
188
|
+
return this.request("PUT", "/v1/fleet", req);
|
|
189
|
+
}
|
|
190
|
+
/** Waits until the Message is delivered and returns the Turn that carries it. */
|
|
191
|
+
async deliveredIn(computer, id, signal) {
|
|
192
|
+
while (!signal?.aborted) {
|
|
193
|
+
for await (const ev of this.stream(computer, `/v1/computers/${enc(computer)}/inbox/${enc(id)}/events`, void 0, {
|
|
194
|
+
signal,
|
|
195
|
+
once: true
|
|
196
|
+
})) {
|
|
197
|
+
const d = ev.data ?? {};
|
|
198
|
+
const type = ev.type;
|
|
199
|
+
const state = type === "snapshot" ? d.message?.state : type;
|
|
200
|
+
const turn = type === "snapshot" ? d.message?.turn : d.turn;
|
|
201
|
+
if (state === "delivered" && turn) return turn;
|
|
202
|
+
if (["withdrawn", "discarded", "expired", "replaced"].includes(state)) {
|
|
203
|
+
throw new A12sError(409, { code: state, message: `message ${id} was ${state} before delivery` });
|
|
204
|
+
}
|
|
205
|
+
if (type === "snapshot" && d.held || type === "held") {
|
|
206
|
+
throw new A12sError(409, {
|
|
207
|
+
code: "held",
|
|
208
|
+
message: `${computer}'s inbox is held (${d.holdReason ?? d.reason}); ${id} waits in the inbox`,
|
|
209
|
+
fix: `a12s inbox resume ${computer}`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
await sleep(1e3, signal);
|
|
214
|
+
}
|
|
215
|
+
throw signal.reason ?? new Error("aborted");
|
|
216
|
+
}
|
|
217
|
+
async *stream(computer, path, after, opts) {
|
|
218
|
+
let cursor = after;
|
|
219
|
+
for (; ; ) {
|
|
220
|
+
if (opts.signal?.aborted) return;
|
|
221
|
+
let res;
|
|
222
|
+
try {
|
|
223
|
+
res = await this.fetchImpl(this.url + path, {
|
|
224
|
+
headers: this.headers({
|
|
225
|
+
accept: "text/event-stream",
|
|
226
|
+
...cursor !== void 0 ? { "last-event-id": String(cursor) } : {}
|
|
227
|
+
}),
|
|
228
|
+
signal: opts.signal
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (opts.signal?.aborted) return;
|
|
232
|
+
if (!isNetworkError(err)) throw err;
|
|
233
|
+
await sleep(opts.reconnectMs ?? 1e3, opts.signal);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!res.ok) {
|
|
237
|
+
const err = new A12sError(res.status, parseError(res.status, await res.text()));
|
|
238
|
+
if (res.status !== 503 && res.status !== 502) throw err;
|
|
239
|
+
opts.live?.();
|
|
240
|
+
await sleep(opts.reconnectMs ?? 1e3, opts.signal);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!res.body) throw new A12sStreamError(`event stream ${path}: response has no body`);
|
|
244
|
+
const frames = readSSE(res.body);
|
|
245
|
+
try {
|
|
246
|
+
for (; ; ) {
|
|
247
|
+
let r;
|
|
248
|
+
try {
|
|
249
|
+
r = await frames.next();
|
|
250
|
+
} catch {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
if (r.done) break;
|
|
254
|
+
const f = r.value;
|
|
255
|
+
if (f.event === "head") {
|
|
256
|
+
const seq = parseJSON(f.data)?.seq;
|
|
257
|
+
if (typeof seq === "number") cursor = Math.max(cursor ?? 0, seq);
|
|
258
|
+
opts.live?.();
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (f.event === "gap") {
|
|
262
|
+
const server = parseJSON(f.data);
|
|
263
|
+
const data = { ...server && typeof server === "object" ? server : {}, after: cursor };
|
|
264
|
+
cursor = void 0;
|
|
265
|
+
if (opts.onGap === "throw") throw new A12sGapError(data);
|
|
266
|
+
const gap = { v: 1, computer, session: "", turn: "", seq: 0, ts: (/* @__PURE__ */ new Date()).toISOString(), type: "gap", data };
|
|
267
|
+
yield gap;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (f.event || !f.data) continue;
|
|
271
|
+
let ev;
|
|
272
|
+
try {
|
|
273
|
+
ev = JSON.parse(f.data);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
throw new A12sStreamError(`malformed event after seq ${cursor ?? "(start)"}: ${f.data.slice(0, 120)}`, {
|
|
276
|
+
cause: err
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (f.id !== void 0) cursor = Number(f.id);
|
|
280
|
+
yield ev;
|
|
281
|
+
}
|
|
282
|
+
} finally {
|
|
283
|
+
await frames.return(void 0);
|
|
284
|
+
res.body.cancel().catch(() => {
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (opts.signal?.aborted) return;
|
|
288
|
+
if (opts.once) return;
|
|
289
|
+
await sleep(opts.reconnectMs ?? 1e3, opts.signal);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
var EventStream = class {
|
|
294
|
+
constructor(source, names, signal) {
|
|
295
|
+
this.source = source;
|
|
296
|
+
this.names = names;
|
|
297
|
+
this.readyP = new Promise((resolve, reject) => {
|
|
298
|
+
this.resolveReady = () => {
|
|
299
|
+
this.isReady = true;
|
|
300
|
+
resolve();
|
|
301
|
+
};
|
|
302
|
+
this.rejectReady = reject;
|
|
303
|
+
});
|
|
304
|
+
this.readyP.catch(() => {
|
|
305
|
+
});
|
|
306
|
+
if (signal?.aborted) this.ac.abort();
|
|
307
|
+
signal?.addEventListener("abort", () => this.close(), { once: true });
|
|
308
|
+
}
|
|
309
|
+
ac = new AbortController();
|
|
310
|
+
queue = [];
|
|
311
|
+
started;
|
|
312
|
+
remaining;
|
|
313
|
+
failure;
|
|
314
|
+
wake;
|
|
315
|
+
readyP;
|
|
316
|
+
resolveReady;
|
|
317
|
+
rejectReady;
|
|
318
|
+
isReady = false;
|
|
319
|
+
/**
|
|
320
|
+
* Resolves once every stream is connected and live (a sleeping Computer counts: its events start when it wakes).
|
|
321
|
+
* Events that happen after this are never missed.
|
|
322
|
+
*/
|
|
323
|
+
ready() {
|
|
324
|
+
this.start();
|
|
325
|
+
return this.readyP;
|
|
326
|
+
}
|
|
327
|
+
close() {
|
|
328
|
+
this.ac.abort();
|
|
329
|
+
this.resolveReady();
|
|
330
|
+
this.notify();
|
|
331
|
+
}
|
|
332
|
+
async next() {
|
|
333
|
+
this.start();
|
|
334
|
+
for (; ; ) {
|
|
335
|
+
if (this.queue.length) return { value: this.queue.shift(), done: false };
|
|
336
|
+
if (this.failure) {
|
|
337
|
+
const { error } = this.failure;
|
|
338
|
+
this.failure = void 0;
|
|
339
|
+
this.close();
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
if (this.ac.signal.aborted || this.remaining === 0) return { value: void 0, done: true };
|
|
343
|
+
await new Promise((r) => this.wake = r);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async return() {
|
|
347
|
+
this.close();
|
|
348
|
+
return { value: void 0, done: true };
|
|
349
|
+
}
|
|
350
|
+
async throw(err) {
|
|
351
|
+
this.close();
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
[Symbol.asyncIterator]() {
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
start() {
|
|
358
|
+
this.started ??= (async () => {
|
|
359
|
+
const names = typeof this.names === "function" ? await this.names() : this.names;
|
|
360
|
+
this.remaining = names.length;
|
|
361
|
+
let live = 0;
|
|
362
|
+
if (names.length === 0) this.resolveReady();
|
|
363
|
+
for (const name of names) {
|
|
364
|
+
let counted = false;
|
|
365
|
+
const onLive = () => {
|
|
366
|
+
if (counted) return;
|
|
367
|
+
counted = true;
|
|
368
|
+
if (++live === names.length) this.resolveReady();
|
|
369
|
+
};
|
|
370
|
+
void this.pump(name, onLive);
|
|
371
|
+
}
|
|
372
|
+
this.notify();
|
|
373
|
+
})().catch((err) => this.fail(err));
|
|
374
|
+
}
|
|
375
|
+
async pump(name, onLive) {
|
|
376
|
+
try {
|
|
377
|
+
for await (const ev of this.source(name, onLive, this.ac.signal)) {
|
|
378
|
+
this.queue.push(ev);
|
|
379
|
+
this.notify();
|
|
380
|
+
}
|
|
381
|
+
} catch (err) {
|
|
382
|
+
if (!this.ac.signal.aborted) this.fail(err);
|
|
383
|
+
} finally {
|
|
384
|
+
onLive();
|
|
385
|
+
this.remaining = (this.remaining ?? 1) - 1;
|
|
386
|
+
this.notify();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
fail(err) {
|
|
390
|
+
this.failure ??= { error: err };
|
|
391
|
+
if (!this.isReady) this.rejectReady(err);
|
|
392
|
+
this.notify();
|
|
393
|
+
}
|
|
394
|
+
notify() {
|
|
395
|
+
const w = this.wake;
|
|
396
|
+
this.wake = void 0;
|
|
397
|
+
w?.();
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var TurnStream = class {
|
|
401
|
+
constructor(events) {
|
|
402
|
+
this.events = events;
|
|
403
|
+
}
|
|
404
|
+
builder = new TurnBuilder();
|
|
405
|
+
/** See {@link EventStream.ready}. */
|
|
406
|
+
ready() {
|
|
407
|
+
return this.events.ready();
|
|
408
|
+
}
|
|
409
|
+
close() {
|
|
410
|
+
this.events.close();
|
|
411
|
+
}
|
|
412
|
+
async next() {
|
|
413
|
+
for (; ; ) {
|
|
414
|
+
const r = await this.events.next();
|
|
415
|
+
if (r.done) return { value: void 0, done: true };
|
|
416
|
+
const turn = this.builder.add(r.value);
|
|
417
|
+
if (turn) return { value: turn, done: false };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async return() {
|
|
421
|
+
this.close();
|
|
422
|
+
return { value: void 0, done: true };
|
|
423
|
+
}
|
|
424
|
+
[Symbol.asyncIterator]() {
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
var TurnBuilder = class {
|
|
429
|
+
open = /* @__PURE__ */ new Map();
|
|
430
|
+
add(ev) {
|
|
431
|
+
if (!ev.turn) return void 0;
|
|
432
|
+
const key = `${ev.computer}\0${ev.turn}`;
|
|
433
|
+
let t = this.open.get(key);
|
|
434
|
+
if (!t) this.open.set(key, t = { text: "", afterTool: false, messages: [], tools: /* @__PURE__ */ new Map() });
|
|
435
|
+
switch (ev.type) {
|
|
436
|
+
case "turn.started":
|
|
437
|
+
t.messages = ev.data.inbox?.messages ?? [];
|
|
438
|
+
t.startedAt = ev.ts;
|
|
439
|
+
return void 0;
|
|
440
|
+
case "tool.started":
|
|
441
|
+
t.afterTool = true;
|
|
442
|
+
t.tools.set(ev.data.id, { id: ev.data.id, name: ev.data.name, input: ev.data.input, isError: false });
|
|
443
|
+
return void 0;
|
|
444
|
+
case "tool.finished": {
|
|
445
|
+
const started = t.tools.get(ev.data.id);
|
|
446
|
+
t.tools.set(ev.data.id, { id: ev.data.id, name: ev.data.name, input: started?.input, output: ev.data.output, isError: ev.data.isError });
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
case "text.delta":
|
|
450
|
+
if (t.afterTool && t.text && !t.text.endsWith("\n")) t.text += "\n";
|
|
451
|
+
t.afterTool = false;
|
|
452
|
+
t.text += ev.data.text;
|
|
453
|
+
return void 0;
|
|
454
|
+
case "turn.finished":
|
|
455
|
+
this.open.delete(key);
|
|
456
|
+
return {
|
|
457
|
+
computer: ev.computer,
|
|
458
|
+
id: ev.turn,
|
|
459
|
+
session: ev.session,
|
|
460
|
+
status: ev.data.status,
|
|
461
|
+
text: t.text,
|
|
462
|
+
...ev.data.error ? { error: ev.data.error } : {},
|
|
463
|
+
messages: t.messages,
|
|
464
|
+
tools: [...t.tools.values()],
|
|
465
|
+
usage: ev.data.usage,
|
|
466
|
+
...t.startedAt ? { startedAt: t.startedAt } : {},
|
|
467
|
+
finishedAt: ev.ts
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
function isNetworkError(err) {
|
|
474
|
+
return err instanceof TypeError || err instanceof Error && err.name === "AbortError";
|
|
475
|
+
}
|
|
476
|
+
function parseJSON(text) {
|
|
477
|
+
try {
|
|
478
|
+
return text ? JSON.parse(text) : void 0;
|
|
479
|
+
} catch {
|
|
480
|
+
return text;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function parseError(status, text) {
|
|
484
|
+
try {
|
|
485
|
+
const b = JSON.parse(text);
|
|
486
|
+
if (b && typeof b.code === "string") return b;
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
return { code: `http_${status}`, message: text.trim() || `HTTP ${status}` };
|
|
490
|
+
}
|
|
491
|
+
function sleep(ms, signal) {
|
|
492
|
+
return new Promise((resolve) => {
|
|
493
|
+
const t = setTimeout(resolve, ms);
|
|
494
|
+
signal?.addEventListener(
|
|
495
|
+
"abort",
|
|
496
|
+
() => {
|
|
497
|
+
clearTimeout(t);
|
|
498
|
+
resolve();
|
|
499
|
+
},
|
|
500
|
+
{ once: true }
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/index.ts
|
|
506
|
+
async function connect(opts = {}) {
|
|
507
|
+
return new A12sClient(opts);
|
|
508
|
+
}
|
|
509
|
+
export {
|
|
510
|
+
A12sClient,
|
|
511
|
+
A12sError,
|
|
512
|
+
A12sGapError,
|
|
513
|
+
A12sStreamError,
|
|
514
|
+
EventStream,
|
|
515
|
+
TurnStream,
|
|
516
|
+
connect
|
|
517
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { A12sEvent, ApiErrorBody, ComputerStatus, EnqueueRequest, EnqueueResponse, GapData, InboxListOptions, InboxListResponse, Message, PutFleetRequest, PutFleetResponse, ServiceStatus, StatusResponse, Token, TokenRequest, Turn } from "./types.js";
|
|
2
|
+
export interface A12sClientOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The Controller's API, e.g. http://127.0.0.1:52011. Default "": the page's own origin, which is what a browser
|
|
5
|
+
* app served by `a12s gateway` uses.
|
|
6
|
+
*/
|
|
7
|
+
url?: string;
|
|
8
|
+
/** The owner token, or a scoped token from `a12s token create`. Not needed behind `a12s gateway`. */
|
|
9
|
+
token?: string;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/** A refused call: the HTTP status and the Controller's error, including its fix line. */
|
|
13
|
+
export declare class A12sError extends Error {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
/** A stable code such as `busy`, `asleep`, `no_grant`, `inbox_full`, `not_found` (see docs/api.md#errors). */
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly fix?: string;
|
|
18
|
+
readonly details?: Record<string, string>;
|
|
19
|
+
constructor(status: number, body: ApiErrorBody);
|
|
20
|
+
}
|
|
21
|
+
/** Thrown by an event stream with `onGap: "throw"` when events were lost. */
|
|
22
|
+
export declare class A12sGapError extends Error {
|
|
23
|
+
readonly after?: number;
|
|
24
|
+
readonly data: GapData;
|
|
25
|
+
constructor(data: GapData);
|
|
26
|
+
}
|
|
27
|
+
/** An event stream received a frame that is not a valid journal event. */
|
|
28
|
+
export declare class A12sStreamError extends Error {
|
|
29
|
+
constructor(message: string, options?: {
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export interface EventsOptions {
|
|
34
|
+
/** One Computer only: replay this Turn from its start, then follow it. */
|
|
35
|
+
turn?: string;
|
|
36
|
+
/** One Computer only: resume after this journal seq. Otherwise a stream starts live. */
|
|
37
|
+
after?: number;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
/** Delay before reconnecting a dropped stream. Default 1000 ms. */
|
|
40
|
+
reconnectMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* When a stream can't resume from its cursor (the journal was reset): "yield" (default) yields a `gap` event and
|
|
43
|
+
* carries on live; "throw" throws {@link A12sGapError}. Either way events were lost: resync from `status()`.
|
|
44
|
+
*/
|
|
45
|
+
onGap?: "yield" | "throw";
|
|
46
|
+
}
|
|
47
|
+
export interface PromptOptions extends Omit<EnqueueRequest, "body"> {
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
/** Every event of the Turn, as it streams. */
|
|
50
|
+
onEvent?: (ev: A12sEvent) => void;
|
|
51
|
+
}
|
|
52
|
+
/** What `prompt()` resolves with: the finished Turn and the Message that started it. */
|
|
53
|
+
export interface PromptResult extends Turn {
|
|
54
|
+
message: EnqueueResponse;
|
|
55
|
+
}
|
|
56
|
+
export declare class A12sClient {
|
|
57
|
+
readonly url: string;
|
|
58
|
+
private readonly token?;
|
|
59
|
+
private readonly fetchImpl;
|
|
60
|
+
constructor(opts?: A12sClientOptions);
|
|
61
|
+
private headers;
|
|
62
|
+
private request;
|
|
63
|
+
/** Every Computer and Service: phase, session, active Turn, inbox, power. */
|
|
64
|
+
status(): Promise<StatusResponse>;
|
|
65
|
+
computer(name: string): Promise<ComputerStatus>;
|
|
66
|
+
/**
|
|
67
|
+
* Puts a Message in a Computer's inbox and returns at once. It is delivered at the Computer's next idle moment,
|
|
68
|
+
* batched with anything else pending. Works while the Computer is busy or asleep.
|
|
69
|
+
*/
|
|
70
|
+
send(computer: string, msg: EnqueueRequest | string): Promise<EnqueueResponse>;
|
|
71
|
+
/** Sends a Message, waits for the Turn that delivers it and resolves when that Turn ends: what `a12s prompt` does. */
|
|
72
|
+
prompt(computer: string, body: string, opts?: PromptOptions): Promise<PromptResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Live events of one Computer, several, or (no argument) every Computer in the Fleet, merged. Each event carries
|
|
75
|
+
* its `computer`. Reconnects after a network drop without losing or repeating events. Call `await stream.ready()`
|
|
76
|
+
* before sending work you want to see from the start; `close()` (or `break`) ends it.
|
|
77
|
+
*/
|
|
78
|
+
events(computers?: string | string[], opts?: EventsOptions): EventStream;
|
|
79
|
+
/**
|
|
80
|
+
* Finished Turns of one Computer, several or all, as they end: who sent the work (`messages`), what the agent said
|
|
81
|
+
* (`text`) and how it ended (`status`). Same `ready()` and `close()` as {@link events}.
|
|
82
|
+
*/
|
|
83
|
+
turns(computers?: string | string[], opts?: Omit<EventsOptions, "turn" | "after">): TurnStream;
|
|
84
|
+
inbox(computer: string, opts?: InboxListOptions): Promise<InboxListResponse>;
|
|
85
|
+
message(computer: string, id: string): Promise<Message>;
|
|
86
|
+
/** Withdraws a pending Message; a 409 A12sError names its state if it is too late. */
|
|
87
|
+
withdraw(computer: string, id: string): Promise<Message>;
|
|
88
|
+
/** Interrupts a Turn (default: the active one). The Turn ends `interrupted`; nothing is replayed. */
|
|
89
|
+
stop(computer: string, turn?: string): Promise<void>;
|
|
90
|
+
/** Stops the Computer's processes and keeps its disks, session and inbox. */
|
|
91
|
+
sleep(computer: string): Promise<void>;
|
|
92
|
+
/** Starts a sleeping Computer; its session resumes on the next Turn. */
|
|
93
|
+
wake(computer: string): Promise<void>;
|
|
94
|
+
services(): Promise<ServiceStatus[]>;
|
|
95
|
+
service(name: string): Promise<ServiceStatus>;
|
|
96
|
+
/** A scoped token for another program; owner only. The secret is in the response once. */
|
|
97
|
+
createToken(req: TokenRequest): Promise<Token>;
|
|
98
|
+
/** What `a12s up` sends: validate and apply a whole Fleet. Most apps leave this to the CLI. */
|
|
99
|
+
putFleet(req: PutFleetRequest): Promise<PutFleetResponse>;
|
|
100
|
+
/** Waits until the Message is delivered and returns the Turn that carries it. */
|
|
101
|
+
private deliveredIn;
|
|
102
|
+
private stream;
|
|
103
|
+
}
|
|
104
|
+
type Source = (computer: string, live: () => void, signal: AbortSignal) => AsyncGenerator<A12sEvent>;
|
|
105
|
+
/** Events of one or more Computers, merged in arrival order. Iterate it with `for await`. */
|
|
106
|
+
export declare class EventStream implements AsyncIterableIterator<A12sEvent> {
|
|
107
|
+
private readonly source;
|
|
108
|
+
private readonly names;
|
|
109
|
+
private readonly ac;
|
|
110
|
+
private readonly queue;
|
|
111
|
+
private started?;
|
|
112
|
+
private remaining?;
|
|
113
|
+
private failure;
|
|
114
|
+
private wake?;
|
|
115
|
+
private readonly readyP;
|
|
116
|
+
private resolveReady;
|
|
117
|
+
private rejectReady;
|
|
118
|
+
private isReady;
|
|
119
|
+
constructor(source: Source, names: string[] | (() => Promise<string[]>), signal?: AbortSignal);
|
|
120
|
+
/**
|
|
121
|
+
* Resolves once every stream is connected and live (a sleeping Computer counts: its events start when it wakes).
|
|
122
|
+
* Events that happen after this are never missed.
|
|
123
|
+
*/
|
|
124
|
+
ready(): Promise<void>;
|
|
125
|
+
close(): void;
|
|
126
|
+
next(): Promise<IteratorResult<A12sEvent>>;
|
|
127
|
+
return(): Promise<IteratorResult<A12sEvent>>;
|
|
128
|
+
throw(err?: unknown): Promise<IteratorResult<A12sEvent>>;
|
|
129
|
+
[Symbol.asyncIterator](): this;
|
|
130
|
+
private start;
|
|
131
|
+
private pump;
|
|
132
|
+
private fail;
|
|
133
|
+
private notify;
|
|
134
|
+
}
|
|
135
|
+
/** Finished Turns of one or more Computers. Iterate it with `for await`. */
|
|
136
|
+
export declare class TurnStream implements AsyncIterableIterator<Turn> {
|
|
137
|
+
private readonly events;
|
|
138
|
+
private readonly builder;
|
|
139
|
+
constructor(events: EventStream);
|
|
140
|
+
/** See {@link EventStream.ready}. */
|
|
141
|
+
ready(): Promise<void>;
|
|
142
|
+
close(): void;
|
|
143
|
+
next(): Promise<IteratorResult<Turn>>;
|
|
144
|
+
return(): Promise<IteratorResult<Turn>>;
|
|
145
|
+
[Symbol.asyncIterator](): this;
|
|
146
|
+
}
|
|
147
|
+
export {};
|