@spredr/sdk 0.0.0-stage → 0.1.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/LICENSE +33 -0
- package/README.md +423 -2
- package/TRADEMARKS.md +18 -0
- package/dist/agent.d.ts +96 -0
- package/dist/caller.d.ts +30 -0
- package/dist/context.d.ts +108 -0
- package/dist/errors.d.ts +45 -0
- package/dist/http.d.ts +48 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +969 -0
- package/dist/owner.d.ts +162 -0
- package/dist/schema.d.ts +28 -0
- package/dist/sign.d.ts +26 -0
- package/dist/tag.d.ts +7 -0
- package/dist/tasks.d.ts +98 -0
- package/dist/types.d.ts +293 -0
- package/dist/usage.d.ts +3 -0
- package/package.json +51 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
// src/agent.ts
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var SpredrError = class extends Error {
|
|
6
|
+
/** The HTTP status ai answered with; 0 when no answer came. */
|
|
7
|
+
status;
|
|
8
|
+
/** ai's trace id for this request, to quote when you write to spredr. */
|
|
9
|
+
trace;
|
|
10
|
+
constructor(status, message, trace = "") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.trace = trace;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var BadRequestError = class extends SpredrError {
|
|
18
|
+
};
|
|
19
|
+
var AuthenticationError = class extends SpredrError {
|
|
20
|
+
};
|
|
21
|
+
var PermissionDeniedError = class extends SpredrError {
|
|
22
|
+
};
|
|
23
|
+
var NotFoundError = class extends SpredrError {
|
|
24
|
+
};
|
|
25
|
+
var ConflictError = class extends SpredrError {
|
|
26
|
+
};
|
|
27
|
+
var RateLimitError = class extends SpredrError {
|
|
28
|
+
/** Seconds ai asked to wait, when it said. */
|
|
29
|
+
retryAfter;
|
|
30
|
+
constructor(status, message, trace = "", retryAfter = null) {
|
|
31
|
+
super(status, message, trace);
|
|
32
|
+
this.retryAfter = retryAfter;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var InternalServerError = class extends SpredrError {
|
|
36
|
+
};
|
|
37
|
+
var ConnectionError = class extends SpredrError {
|
|
38
|
+
};
|
|
39
|
+
var TimeoutError = class extends ConnectionError {
|
|
40
|
+
};
|
|
41
|
+
var TaskError = class extends Error {
|
|
42
|
+
task;
|
|
43
|
+
constructor(task) {
|
|
44
|
+
super(task.status === "failed" ? task.error || "The task failed." : task.status === "paused" ? "The task waits for an answer: see pauses()." : `The task is ${task.status}.`);
|
|
45
|
+
this.name = "TaskError";
|
|
46
|
+
this.task = task;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var BY_STATUS = /* @__PURE__ */ new Map([[400, BadRequestError], [401, AuthenticationError], [403, PermissionDeniedError], [404, NotFoundError], [409, ConflictError]]);
|
|
50
|
+
function errorFor(status, message, trace, retryAfter) {
|
|
51
|
+
if (status === 429) return new RateLimitError(status, message, trace, retryAfter);
|
|
52
|
+
if (status >= 500) return new InternalServerError(status, message, trace);
|
|
53
|
+
const Class = BY_STATUS.get(status) ?? SpredrError;
|
|
54
|
+
return new Class(status, message, trace);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/sign.ts
|
|
58
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";
|
|
59
|
+
var HEADERS = { from: "x-spredr-from", time: "x-spredr-time", nonce: "x-spredr-nonce", signature: "x-spredr-signature" };
|
|
60
|
+
var AI_SIGNER = "ai";
|
|
61
|
+
var SKEW_MS = 5 * 6e4;
|
|
62
|
+
var NONCE = /^[0-9a-f]{32}$/;
|
|
63
|
+
var privateKeyOf = (base64) => createPrivateKey({ key: Buffer.from(base64, "base64"), format: "der", type: "pkcs8" });
|
|
64
|
+
var publicKeyOf = (base64) => createPublicKey({ key: Buffer.from(base64, "base64"), format: "der", type: "spki" });
|
|
65
|
+
function newKeys() {
|
|
66
|
+
const pair = generateKeyPairSync("ed25519");
|
|
67
|
+
return { privateKey: pair.privateKey.export({ format: "der", type: "pkcs8" }).toString("base64"), publicKey: pair.publicKey.export({ format: "der", type: "spki" }).toString("base64") };
|
|
68
|
+
}
|
|
69
|
+
var bytesToSign = (method, path, time, nonce, body) => Buffer.from([method.toUpperCase(), path, time, nonce, createHash("sha256").update(body).digest("hex")].join("\n"));
|
|
70
|
+
function signedHeaders(from, key, method, path, body) {
|
|
71
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
72
|
+
const nonce = randomBytes(16).toString("hex");
|
|
73
|
+
return { [HEADERS.from]: from, [HEADERS.time]: time, [HEADERS.nonce]: nonce, [HEADERS.signature]: sign(null, bytesToSign(method, path, time, nonce, body), key).toString("base64") };
|
|
74
|
+
}
|
|
75
|
+
function aiVerifier(aiPublicKey) {
|
|
76
|
+
const key = publicKeyOf(aiPublicKey);
|
|
77
|
+
const seen = /* @__PURE__ */ new Map();
|
|
78
|
+
return (request) => {
|
|
79
|
+
const header = (name) => {
|
|
80
|
+
const value = request.headers[name];
|
|
81
|
+
return typeof value === "string" ? value : "";
|
|
82
|
+
};
|
|
83
|
+
const time = header(HEADERS.time);
|
|
84
|
+
const nonce = header(HEADERS.nonce);
|
|
85
|
+
const at = Date.parse(time);
|
|
86
|
+
if (header(HEADERS.from) !== AI_SIGNER || !Number.isFinite(at) || Math.abs(Date.now() - at) > SKEW_MS || !NONCE.test(nonce) || seen.has(nonce)) return false;
|
|
87
|
+
let genuine = false;
|
|
88
|
+
try {
|
|
89
|
+
genuine = verify(null, bytesToSign(request.method, request.path, time, nonce, request.body), key, Buffer.from(header(HEADERS.signature), "base64"));
|
|
90
|
+
} catch {
|
|
91
|
+
genuine = false;
|
|
92
|
+
}
|
|
93
|
+
if (!genuine) return false;
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
for (const [old, until] of seen) {
|
|
96
|
+
if (until > now) break;
|
|
97
|
+
seen.delete(old);
|
|
98
|
+
}
|
|
99
|
+
seen.set(nonce, now + 2 * SKEW_MS);
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/http.ts
|
|
105
|
+
var STREAM_TIMEOUT_MS = 11 * 6e4;
|
|
106
|
+
var TIMEOUT_MS = 75e3;
|
|
107
|
+
var MAX_RETRIES = 2;
|
|
108
|
+
var RETRY_DELAY_MS = 500;
|
|
109
|
+
var MAX_DELAY_MS = 8e3;
|
|
110
|
+
var MAX_RETRY_AFTER_S = 60;
|
|
111
|
+
var REPEATABLE = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
|
|
112
|
+
var fieldOf = (parsed, name) => typeof parsed === "object" && parsed !== null && name in parsed && typeof parsed[name] === "string" ? String(parsed[name]) : "";
|
|
113
|
+
function parseJson(raw) {
|
|
114
|
+
try {
|
|
115
|
+
return { ok: true, value: raw ? JSON.parse(raw) : null };
|
|
116
|
+
} catch {
|
|
117
|
+
return { ok: false };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function retryAfterOf(response) {
|
|
121
|
+
const header = response.headers.get("retry-after");
|
|
122
|
+
if (!header) return null;
|
|
123
|
+
const seconds = Number(header);
|
|
124
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds);
|
|
125
|
+
const at = Date.parse(header);
|
|
126
|
+
return Number.isFinite(at) ? Math.max(0, (at - Date.now()) / 1e3) : null;
|
|
127
|
+
}
|
|
128
|
+
async function failureOf(response) {
|
|
129
|
+
const raw = await response.text();
|
|
130
|
+
const parsed = parseJson(raw);
|
|
131
|
+
const message = (parsed.ok ? fieldOf(parsed.value, "error") : "") || raw.slice(0, 300).trim() || `HTTP ${response.status}`;
|
|
132
|
+
return errorFor(response.status, message, parsed.ok ? fieldOf(parsed.value, "trace") : "", retryAfterOf(response));
|
|
133
|
+
}
|
|
134
|
+
function client(base, auth, options = {}) {
|
|
135
|
+
const root = base.replace(/\/+$/, "");
|
|
136
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
137
|
+
const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
|
|
138
|
+
const maxRetries = Math.max(0, options.maxRetries ?? MAX_RETRIES);
|
|
139
|
+
const firstDelay = options.retryDelayMs ?? RETRY_DELAY_MS;
|
|
140
|
+
const headersFor = (method, path, text, json) => {
|
|
141
|
+
const headers = { accept: json ? "application/json" : "*/*", ...typeof text === "string" && text ? { "content-type": "application/json" } : {} };
|
|
142
|
+
if ("token" in auth) headers.authorization = `Bearer ${auth.token}`;
|
|
143
|
+
else Object.assign(headers, signedHeaders(auth.from, auth.key, method, path, text));
|
|
144
|
+
return headers;
|
|
145
|
+
};
|
|
146
|
+
const waitBefore = (tryNumber, retryAfter) => {
|
|
147
|
+
const backoff = Math.min(MAX_DELAY_MS, firstDelay * 2 ** tryNumber) * (0.75 + Math.random() * 0.5);
|
|
148
|
+
return new Promise((resolve) => setTimeout(resolve, retryAfter === null ? backoff : retryAfter * 1e3));
|
|
149
|
+
};
|
|
150
|
+
async function send(method, path, text, json, repeatable, extra = {}, waitMs = timeoutMs) {
|
|
151
|
+
for (let tryNumber = 0; ; tryNumber++) {
|
|
152
|
+
const last = tryNumber >= maxRetries;
|
|
153
|
+
let response;
|
|
154
|
+
try {
|
|
155
|
+
response = await fetcher(`${root}${path}`, { method, headers: { ...headersFor(method, path, text, json), ...extra }, ...text.length ? { body: typeof text === "string" ? text : new Uint8Array(text) } : {}, signal: AbortSignal.timeout(waitMs) });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
158
|
+
if (!repeatable || last) {
|
|
159
|
+
throw timedOut ? new TimeoutError(0, `ai did not answer in ${timeoutMs} ms (${method} ${path}).`) : new ConnectionError(0, `ai could not be reached (${method} ${path}): ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
}
|
|
161
|
+
await waitBefore(tryNumber, null);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (response.ok || last) return response;
|
|
165
|
+
const retryAfter = retryAfterOf(response);
|
|
166
|
+
const again = response.status === 429 ? retryAfter !== null && retryAfter <= MAX_RETRY_AFTER_S : repeatable && (response.status === 408 || response.status >= 500);
|
|
167
|
+
if (!again) return response;
|
|
168
|
+
await response.body?.cancel();
|
|
169
|
+
await waitBefore(tryNumber, retryAfter);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
async text(method, path) {
|
|
174
|
+
const response = await send(method, path, "", false, true);
|
|
175
|
+
if (!response.ok) throw await failureOf(response);
|
|
176
|
+
return response.text();
|
|
177
|
+
},
|
|
178
|
+
async call(method, path, body, callOptions = {}) {
|
|
179
|
+
const text = body === void 0 ? "" : JSON.stringify(body);
|
|
180
|
+
const response = await send(method, path, text, true, callOptions.idempotent ?? REPEATABLE.has(method));
|
|
181
|
+
if (!response.ok) throw await failureOf(response);
|
|
182
|
+
const parsed = parseJson(await response.text());
|
|
183
|
+
if (!parsed.ok) throw new SpredrError(response.status, `ai answered ${method} ${path} with something that is not JSON.`);
|
|
184
|
+
return parsed.value;
|
|
185
|
+
},
|
|
186
|
+
async upload(path, data, headers) {
|
|
187
|
+
const response = await send("PUT", path, data, true, true, { "content-type": "application/octet-stream", ...headers });
|
|
188
|
+
if (!response.ok) throw await failureOf(response);
|
|
189
|
+
const parsed = parseJson(await response.text());
|
|
190
|
+
if (!parsed.ok) throw new SpredrError(response.status, `ai answered PUT ${path} with something that is not JSON.`);
|
|
191
|
+
return parsed.value;
|
|
192
|
+
},
|
|
193
|
+
async *events(path, lastId) {
|
|
194
|
+
const response = await send("GET", path, "", false, true, { accept: "text/event-stream", ...lastId ? { "last-event-id": String(lastId) } : {} }, STREAM_TIMEOUT_MS);
|
|
195
|
+
if (!response.ok) throw await failureOf(response);
|
|
196
|
+
if (!response.body) return;
|
|
197
|
+
const reader = response.body.getReader();
|
|
198
|
+
const decoder = new TextDecoder();
|
|
199
|
+
let buffered = "";
|
|
200
|
+
for (; ; ) {
|
|
201
|
+
const { value, done } = await reader.read();
|
|
202
|
+
if (done) return;
|
|
203
|
+
buffered += decoder.decode(value, { stream: true });
|
|
204
|
+
for (let end = buffered.indexOf("\n\n"); end >= 0; end = buffered.indexOf("\n\n")) {
|
|
205
|
+
const block = buffered.slice(0, end);
|
|
206
|
+
buffered = buffered.slice(end + 2);
|
|
207
|
+
const field = (name) => block.split("\n").filter((line) => line.startsWith(`${name}:`)).map((line) => line.slice(name.length + 1).trimStart()).join("\n");
|
|
208
|
+
const data = field("data");
|
|
209
|
+
if (data) yield { id: Number(field("id")) || 0, event: field("event") || "message", data };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
async download(path) {
|
|
214
|
+
const response = await send("GET", path, "", false, true);
|
|
215
|
+
if (!response.ok) throw await failureOf(response);
|
|
216
|
+
return { data: new Uint8Array(await response.arrayBuffer()), type: response.headers.get("content-type") ?? "application/octet-stream", headers: response.headers };
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function query(values) {
|
|
221
|
+
const pairs = Object.entries(values).filter((pair) => pair[1] !== void 0).map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`);
|
|
222
|
+
return pairs.length ? `?${pairs.join("&")}` : "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/usage.ts
|
|
226
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
227
|
+
var numberAt = (from, ...names) => {
|
|
228
|
+
for (const name of names) {
|
|
229
|
+
const value = from[name];
|
|
230
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
231
|
+
}
|
|
232
|
+
return void 0;
|
|
233
|
+
};
|
|
234
|
+
function usageOf(answer) {
|
|
235
|
+
if (!isRecord(answer)) return {};
|
|
236
|
+
const response = isRecord(answer.response) ? answer.response : {};
|
|
237
|
+
const model = typeof answer.model === "string" ? answer.model : typeof response.modelId === "string" ? response.modelId : void 0;
|
|
238
|
+
const usage = isRecord(answer.usage) ? answer.usage : {};
|
|
239
|
+
const tokensIn = numberAt(usage, "input_tokens", "prompt_tokens", "inputTokens", "promptTokens");
|
|
240
|
+
const tokensOut = numberAt(usage, "output_tokens", "completion_tokens", "outputTokens", "completionTokens");
|
|
241
|
+
return { ...model ? { model } : {}, ...tokensIn === void 0 ? {} : { tokensIn }, ...tokensOut === void 0 ? {} : { tokensOut } };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/context.ts
|
|
245
|
+
function sameJson(a, b) {
|
|
246
|
+
if (a === b) return true;
|
|
247
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
248
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
249
|
+
const keys = Object.keys(a);
|
|
250
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
251
|
+
return keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && sameJson(Reflect.get(a, key), Reflect.get(b, key)));
|
|
252
|
+
}
|
|
253
|
+
var PAUSED = /* @__PURE__ */ Symbol("paused");
|
|
254
|
+
var MAX_MEMO_BYTES = 256 * 1024;
|
|
255
|
+
var failedStep = (error) => ({ ok: false, detail: error instanceof Error ? error.message.slice(0, 300) : "failed" });
|
|
256
|
+
function memoOf(value) {
|
|
257
|
+
if (value === void 0) return {};
|
|
258
|
+
try {
|
|
259
|
+
const text = JSON.stringify(value);
|
|
260
|
+
if (text === void 0 || Buffer.byteLength(text) > MAX_MEMO_BYTES) return void 0;
|
|
261
|
+
return { value: JSON.parse(text) };
|
|
262
|
+
} catch {
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
var STREAM_FLUSH_MS = 150;
|
|
267
|
+
var STREAM_FLUSH_CHARS = 2e3;
|
|
268
|
+
function contextFor(client2, delivery) {
|
|
269
|
+
const { task } = delivery;
|
|
270
|
+
const call = client2.call.bind(client2);
|
|
271
|
+
const began = Date.now();
|
|
272
|
+
const path = `/agent/tasks/${task.id}`;
|
|
273
|
+
let pending = "";
|
|
274
|
+
let flushTimer = null;
|
|
275
|
+
let sending = Promise.resolve();
|
|
276
|
+
const flushNow = () => {
|
|
277
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
278
|
+
flushTimer = null;
|
|
279
|
+
if (pending) {
|
|
280
|
+
const text = pending;
|
|
281
|
+
pending = "";
|
|
282
|
+
sending = sending.then(() => call("POST", `${path}/delta`, { attempt: task.attempt, text })).then(() => void 0, () => void 0);
|
|
283
|
+
}
|
|
284
|
+
return sending;
|
|
285
|
+
};
|
|
286
|
+
const report = (step) => call("POST", `${path}/steps`, { attempt: task.attempt, steps: [step] }).catch(() => void 0);
|
|
287
|
+
const answers = delivery.answers ?? (delivery.resume ? [delivery.resume] : []);
|
|
288
|
+
const memos = delivery.memo ?? [];
|
|
289
|
+
let nextAnswer = 0;
|
|
290
|
+
let nextMemo = 0;
|
|
291
|
+
const replayed = (key) => {
|
|
292
|
+
const entry = memos[nextMemo];
|
|
293
|
+
if (!entry) return { hit: false };
|
|
294
|
+
if (entry.key !== key) {
|
|
295
|
+
nextMemo = memos.length;
|
|
296
|
+
return { hit: false };
|
|
297
|
+
}
|
|
298
|
+
nextMemo += 1;
|
|
299
|
+
return { hit: true, value: entry.has ? entry.value : void 0 };
|
|
300
|
+
};
|
|
301
|
+
async function once(kind, key, name, work, read2) {
|
|
302
|
+
const memo = replayed(`${kind}:${key}`);
|
|
303
|
+
if (memo.hit) return memo.value;
|
|
304
|
+
const started = Date.now();
|
|
305
|
+
const at = started - began;
|
|
306
|
+
try {
|
|
307
|
+
const value = await work();
|
|
308
|
+
const kept = memoOf(value);
|
|
309
|
+
await report({ kind, name: typeof name === "string" ? name : name(value), ms: Date.now() - started, at, ...read2(value), ...kept ? { memo: { key: `${kind}:${key}`, ...kept } } : {} });
|
|
310
|
+
return value;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
await report({ kind, name: typeof name === "string" ? name : kind, ms: Date.now() - started, at, ...failedStep(error) });
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const tenant = task.tenant ?? "";
|
|
317
|
+
const thread = task.thread ?? null;
|
|
318
|
+
return {
|
|
319
|
+
task,
|
|
320
|
+
tenant,
|
|
321
|
+
thread,
|
|
322
|
+
lessons: delivery.lessons.items,
|
|
323
|
+
trial: task.trial,
|
|
324
|
+
async ask(action, question, extra = {}) {
|
|
325
|
+
const answer = answers[nextAnswer];
|
|
326
|
+
const sameAsked = answer !== void 0 && answer.action === action && (answer.question === void 0 || answer.question === question) && (answer.data === void 0 || sameJson(answer.data, extra.data ?? null));
|
|
327
|
+
if (answer && sameAsked) {
|
|
328
|
+
nextAnswer += 1;
|
|
329
|
+
return answer;
|
|
330
|
+
}
|
|
331
|
+
nextAnswer = answers.length;
|
|
332
|
+
const { pause } = await call("POST", `${path}/pause`, { attempt: task.attempt, action, question, ...extra });
|
|
333
|
+
if (pause.status === "answered" && pause.answer) return pause.answer;
|
|
334
|
+
throw PAUSED;
|
|
335
|
+
},
|
|
336
|
+
stream(text) {
|
|
337
|
+
if (!text) return;
|
|
338
|
+
pending += text;
|
|
339
|
+
if (pending.length >= STREAM_FLUSH_CHARS) void flushNow();
|
|
340
|
+
else if (!flushTimer) flushTimer = setTimeout(() => void flushNow(), STREAM_FLUSH_MS);
|
|
341
|
+
},
|
|
342
|
+
flush: () => flushNow(),
|
|
343
|
+
async progress(data) {
|
|
344
|
+
await call("POST", `${path}/progress`, { attempt: task.attempt, data });
|
|
345
|
+
},
|
|
346
|
+
async step(step) {
|
|
347
|
+
await call("POST", `${path}/steps`, { attempt: task.attempt, steps: [step] });
|
|
348
|
+
},
|
|
349
|
+
track: (kind, name, work, usage) => once(kind, name, name, work, (value) => usage ? usage(value) : {}),
|
|
350
|
+
model: (work, name) => once("model", name ?? "", (value) => name ?? usageOf(value).model ?? "model", work, usageOf),
|
|
351
|
+
later: (kind, input, at, key) => once("other", `later:${kind}`, `later ${kind}`, async () => {
|
|
352
|
+
await call("POST", "/agent/tasks", { kind, input, at: new Date(at).toISOString(), ...key ? { key } : {}, ...tenant ? { tenant } : {}, ...thread ? { thread } : {} }, { idempotent: Boolean(key) });
|
|
353
|
+
}, () => ({})),
|
|
354
|
+
files: delivery.files ?? [],
|
|
355
|
+
async file(idOrName) {
|
|
356
|
+
const found = (delivery.files ?? []).find((one) => one.id === idOrName || one.name === idOrName);
|
|
357
|
+
if (!found) throw new Error(`No file \xAB${idOrName}\xBB in this task's session: its files are ${(delivery.files ?? []).map((one) => one.name).join(", ") || "none"}.`);
|
|
358
|
+
const got = await client2.download(`/agent/files/${encodeURIComponent(found.id)}${query({ task: task.id })}`);
|
|
359
|
+
return { data: got.data, type: got.type, name: found.name };
|
|
360
|
+
},
|
|
361
|
+
memory: {
|
|
362
|
+
remember: (scope, text, extra = {}) => once("memory", `remember:${scope}`, "remember", async () => {
|
|
363
|
+
const answer = await call("PUT", "/agent/memory", { task: task.id, scope, text, ...extra }, { idempotent: Boolean(extra.key) });
|
|
364
|
+
if (!answer.kept) console.warn(`memory: ${answer.why ?? "this fact was not kept"}`);
|
|
365
|
+
return answer.fact;
|
|
366
|
+
}, () => ({})),
|
|
367
|
+
async recall(options) {
|
|
368
|
+
return (await call("GET", `/agent/memory${query({ ...options, task: task.id })}`)).facts;
|
|
369
|
+
},
|
|
370
|
+
async forget(id) {
|
|
371
|
+
await call("DELETE", `/agent/memory/${id}${query({ task: task.id })}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/schema.ts
|
|
378
|
+
async function validated(schema, input) {
|
|
379
|
+
const result = await schema["~standard"].validate(input);
|
|
380
|
+
if (result.issues === void 0) return result.value;
|
|
381
|
+
const where = (issue) => issue.path?.length ? `${issue.path.map((part) => String(typeof part === "object" ? part.key : part)).join(".")}: ` : "";
|
|
382
|
+
throw new Error(`The input does not match: ${result.issues.map((issue) => `${where(issue)}${issue.message}`).join("; ")}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/tag.ts
|
|
386
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
387
|
+
var working = new AsyncLocalStorage();
|
|
388
|
+
var METHODS = ["log", "info", "warn", "error", "debug"];
|
|
389
|
+
var tagged = false;
|
|
390
|
+
function currentTask() {
|
|
391
|
+
return working.getStore() ?? null;
|
|
392
|
+
}
|
|
393
|
+
function asTask(taskId, work) {
|
|
394
|
+
return working.run(taskId, work);
|
|
395
|
+
}
|
|
396
|
+
function tagConsole() {
|
|
397
|
+
if (tagged) return;
|
|
398
|
+
tagged = true;
|
|
399
|
+
for (const method of METHODS) {
|
|
400
|
+
const print = console[method].bind(console);
|
|
401
|
+
console[method] = (...parts) => {
|
|
402
|
+
const id = working.getStore();
|
|
403
|
+
if (!id) return print(...parts);
|
|
404
|
+
const [first, ...rest] = parts;
|
|
405
|
+
return typeof first === "string" ? print(`[${id}] ${first}`, ...rest) : print(`[${id}]`, ...parts);
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/agent.ts
|
|
411
|
+
var MAX_BODY = 4 * 1024 * 1024;
|
|
412
|
+
var HELLO_TRIES = 30;
|
|
413
|
+
var SHUTDOWN_GRACE_MS = 25e3;
|
|
414
|
+
function read(request) {
|
|
415
|
+
return new Promise((resolve) => {
|
|
416
|
+
const chunks = [];
|
|
417
|
+
let size = 0;
|
|
418
|
+
request.on("data", (chunk) => {
|
|
419
|
+
size += chunk.length;
|
|
420
|
+
if (size <= MAX_BODY) chunks.push(chunk);
|
|
421
|
+
});
|
|
422
|
+
request.on("end", () => resolve(size <= MAX_BODY ? Buffer.concat(chunks) : null));
|
|
423
|
+
request.on("error", () => resolve(null));
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
var need = (value, name) => {
|
|
427
|
+
const found = value || process.env[name];
|
|
428
|
+
if (!found) throw new Error(`${name} is not set: register this service as an agent on spredr (it writes the variables), or pass the options.`);
|
|
429
|
+
return found;
|
|
430
|
+
};
|
|
431
|
+
var Agent = class {
|
|
432
|
+
#manifest;
|
|
433
|
+
#id;
|
|
434
|
+
#client;
|
|
435
|
+
#verify;
|
|
436
|
+
#handlers = /* @__PURE__ */ new Map();
|
|
437
|
+
#taskPath = "/spredr/task";
|
|
438
|
+
#log;
|
|
439
|
+
#graceMs;
|
|
440
|
+
#running = /* @__PURE__ */ new Set();
|
|
441
|
+
#server = null;
|
|
442
|
+
#closing = false;
|
|
443
|
+
constructor(options = {}) {
|
|
444
|
+
const { id, key, ai, aiKey, fetch, timeoutMs, maxRetries, retryDelayMs, log, shutdownGraceMs, tagLogs, ...manifest } = options;
|
|
445
|
+
if (tagLogs !== false) tagConsole();
|
|
446
|
+
this.#manifest = manifest;
|
|
447
|
+
const clientOptions = { ...fetch ? { fetch } : {}, ...timeoutMs === void 0 ? {} : { timeoutMs }, ...maxRetries === void 0 ? {} : { maxRetries }, ...retryDelayMs === void 0 ? {} : { retryDelayMs } };
|
|
448
|
+
this.#id = need(id, "SPREDR_AGENT_ID");
|
|
449
|
+
this.#client = client(need(ai, "SPREDR_AI_URL"), { from: this.#id, key: privateKeyOf(need(key, "SPREDR_AGENT_KEY")) }, clientOptions);
|
|
450
|
+
this.#verify = aiVerifier(need(aiKey, "SPREDR_AI_KEY"));
|
|
451
|
+
this.#log = log === false ? () => void 0 : log ?? ((line) => console.log(`spredr agent \xB7 ${line}`));
|
|
452
|
+
this.#graceMs = shutdownGraceMs ?? SHUTDOWN_GRACE_MS;
|
|
453
|
+
}
|
|
454
|
+
on(kind, schemaOrHandler, maybeHandler) {
|
|
455
|
+
const [schema, handler] = typeof schemaOrHandler === "function" ? [null, schemaOrHandler] : [schemaOrHandler, maybeHandler];
|
|
456
|
+
if (!handler) throw new Error(`on("${kind}"): a handler is missing.`);
|
|
457
|
+
const run = async (input, ctx) => handler(schema ? await validated(schema, input) : input, ctx);
|
|
458
|
+
this.#handlers.set(kind, run);
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
/** Says hello with the manifest (retrying while ai is not reachable yet). */
|
|
462
|
+
async hello() {
|
|
463
|
+
const kinds = [...this.#handlers.keys()].filter((kind) => kind !== this.#manifest.judge);
|
|
464
|
+
const declared = this.#manifest.tasks ?? kinds.map((kind) => ({ kind }));
|
|
465
|
+
for (let tries = 1; ; tries++) {
|
|
466
|
+
try {
|
|
467
|
+
await this.#client.call("POST", "/agent/hello", { ...this.#manifest, tasks: declared }, { idempotent: true });
|
|
468
|
+
return;
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (tries >= HELLO_TRIES) throw error;
|
|
471
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/** Listens for tasks (and answers /health), then says hello. On SIGTERM it closes and exits. */
|
|
476
|
+
async listen(port = Number(process.env.PORT ?? 8080)) {
|
|
477
|
+
const server = createServer((request, response) => void this.#serve(request, response));
|
|
478
|
+
await new Promise((resolve) => server.listen(port, resolve));
|
|
479
|
+
this.#server = server;
|
|
480
|
+
this.#log(`listening on ${port}`);
|
|
481
|
+
await this.hello();
|
|
482
|
+
this.#log("said hello to ai");
|
|
483
|
+
process.once("SIGTERM", () => void this.close().then(() => process.exit(0)));
|
|
484
|
+
return server;
|
|
485
|
+
}
|
|
486
|
+
/** Takes no new task, and waits for the running ones to finish (up to shutdownGraceMs). */
|
|
487
|
+
async close() {
|
|
488
|
+
this.#closing = true;
|
|
489
|
+
const closed = this.#server ? new Promise((resolve) => this.#server?.close(() => resolve())) : Promise.resolve();
|
|
490
|
+
const finished = Promise.allSettled([...this.#running]);
|
|
491
|
+
const late = new Promise((resolve) => setTimeout(() => resolve("late"), this.#graceMs).unref());
|
|
492
|
+
if (await Promise.race([finished, late]) === "late") this.#log(`closing with ${this.#running.size} task(s) still running: ai gives them again`);
|
|
493
|
+
this.#server?.closeAllConnections();
|
|
494
|
+
await closed;
|
|
495
|
+
}
|
|
496
|
+
/** Where ai sends its tasks: route this path to `handle()` when the agent lives inside your own server. */
|
|
497
|
+
get path() {
|
|
498
|
+
return this.#taskPath;
|
|
499
|
+
}
|
|
500
|
+
/** One request from ai, answered: for an agent inside a server you already run (Fastify, Express, Hono). Give it the
|
|
501
|
+
* raw body exactly as it arrived, since the signature is over those bytes, and send back what it returns. The task's
|
|
502
|
+
* handler runs after the answer, as with listen(). Call start() once to say hello, and close() when your server stops. */
|
|
503
|
+
async handle(request) {
|
|
504
|
+
const json = (status, body) => ({ status, headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
505
|
+
if (request.method !== "POST" || request.path !== this.#taskPath) return { status: 404, headers: {}, body: "" };
|
|
506
|
+
if (!this.#verify(request)) return json(401, { error: "not signed by ai" });
|
|
507
|
+
if (this.#closing) return json(503, { error: "this agent is closing" });
|
|
508
|
+
const delivery = JSON.parse(typeof request.body === "string" ? request.body : Buffer.from(request.body).toString("utf8"));
|
|
509
|
+
if (delivery.to && delivery.to.agent !== this.#id) return json(403, { error: "this task is addressed to another agent" });
|
|
510
|
+
const handler = this.#handlers.get(delivery.task.kind);
|
|
511
|
+
if (!handler) return json(200, { error: `this agent does not handle \xAB${delivery.task.kind}\xBB tasks` });
|
|
512
|
+
const running = this.#run(handler, delivery).finally(() => this.#running.delete(running));
|
|
513
|
+
this.#running.add(running);
|
|
514
|
+
return { status: 202, headers: {}, body: "" };
|
|
515
|
+
}
|
|
516
|
+
/** Says hello to ai, for an agent inside your own server (listen() does this itself). Your server owns the shutdown:
|
|
517
|
+
* call close() in it, and the running tasks get their grace. */
|
|
518
|
+
async start() {
|
|
519
|
+
await this.hello();
|
|
520
|
+
this.#log("said hello to ai");
|
|
521
|
+
}
|
|
522
|
+
async #serve(request, response) {
|
|
523
|
+
if (request.url === "/health") {
|
|
524
|
+
response.writeHead(this.#closing ? 503 : 200).end(this.#closing ? "closing" : "ok");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const body = request.method === "POST" && request.url === this.#taskPath ? await read(request) : Buffer.alloc(0);
|
|
528
|
+
if (!body) {
|
|
529
|
+
response.writeHead(413).end();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const answer = await this.handle({ method: request.method ?? "", path: request.url ?? "", headers: request.headers, body });
|
|
533
|
+
response.writeHead(answer.status, answer.headers).end(answer.body);
|
|
534
|
+
}
|
|
535
|
+
async #run(handler, delivery) {
|
|
536
|
+
const { task } = delivery;
|
|
537
|
+
const path = `/agent/tasks/${task.id}`;
|
|
538
|
+
const beatMs = Math.max(5e3, (this.#manifest.timeoutSeconds ?? 120) * 1e3 / 3);
|
|
539
|
+
const beat = setInterval(() => void this.#client.call("POST", `${path}/progress`, { attempt: task.attempt, data: { alive: true } }).catch(() => void 0), beatMs).unref();
|
|
540
|
+
let outcome;
|
|
541
|
+
const started = Date.now();
|
|
542
|
+
try {
|
|
543
|
+
const ctx = contextFor(this.#client, delivery);
|
|
544
|
+
try {
|
|
545
|
+
outcome = { result: await asTask(task.id, () => handler(task.input, ctx)) ?? null };
|
|
546
|
+
} finally {
|
|
547
|
+
await ctx.flush();
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (error === PAUSED) {
|
|
551
|
+
this.#log(`${task.id} ${task.kind} waits for an answer`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
555
|
+
outcome = { error: message.slice(0, 2e3) || "failed" };
|
|
556
|
+
} finally {
|
|
557
|
+
clearInterval(beat);
|
|
558
|
+
}
|
|
559
|
+
this.#log("error" in outcome ? `${task.id} ${task.kind} failed: ${outcome.error.slice(0, 200)}` : `${task.id} ${task.kind} done in ${((Date.now() - started) / 1e3).toFixed(1)} s`);
|
|
560
|
+
try {
|
|
561
|
+
await this.#client.call("POST", `${path}/result`, { attempt: task.attempt, ...outcome }, { idempotent: true });
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof ConflictError) return;
|
|
564
|
+
this.#log(`could not report ${task.id}: ${error instanceof Error ? error.message : String(error)}; ai gives it again when its lease ends`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** What apps said about this agent's tasks after `after` (a feedback seq): what it learns from. */
|
|
568
|
+
async feedback(after = 0) {
|
|
569
|
+
return (await this.#client.call("GET", `/agent/feedback${query({ after: after || void 0 })}`)).feedback;
|
|
570
|
+
}
|
|
571
|
+
/** Proposes a lesson to its owner, drawn from feedback. */
|
|
572
|
+
async propose(text, why = "", sources = []) {
|
|
573
|
+
await this.#client.call("POST", "/agent/lessons", { text, why, sources });
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// src/tasks.ts
|
|
578
|
+
var MAX_WAIT_S = 60;
|
|
579
|
+
var Tasks = class {
|
|
580
|
+
#client;
|
|
581
|
+
#agent;
|
|
582
|
+
#tenant;
|
|
583
|
+
constructor(client2, agent, tenant) {
|
|
584
|
+
this.#client = client2;
|
|
585
|
+
this.#agent = agent;
|
|
586
|
+
this.#tenant = tenant;
|
|
587
|
+
}
|
|
588
|
+
/** The same agent, working for one tenant: `caller.for('user_42').result('reply', …)`. One agent serves any number
|
|
589
|
+
* of tenants at once, each with its own memory, questions, freedom, lessons and caps. */
|
|
590
|
+
for(tenant) {
|
|
591
|
+
return new Tenant(this.#client, this.#agent, tenant);
|
|
592
|
+
}
|
|
593
|
+
/** Asks the agent for a task. With `wait`, answers once it is done (or when the wait runs out). With a `key`, asking
|
|
594
|
+
* again is the same task, so it is safe to retry. */
|
|
595
|
+
async ask(kind, input, options = {}) {
|
|
596
|
+
const tenant = options.tenant ?? this.#tenant;
|
|
597
|
+
const body = {
|
|
598
|
+
kind,
|
|
599
|
+
input,
|
|
600
|
+
...options.key ? { key: options.key } : {},
|
|
601
|
+
...options.wait ? { wait: Math.min(options.wait, MAX_WAIT_S) } : {},
|
|
602
|
+
...options.at ? { at: new Date(options.at).toISOString() } : {},
|
|
603
|
+
...options.thread ? { thread: options.thread } : {},
|
|
604
|
+
...tenant ? { tenant } : {}
|
|
605
|
+
};
|
|
606
|
+
return (await this.#client.call("POST", `/agents/${encodeURIComponent(this.#agent)}/tasks`, body, { idempotent: Boolean(options.key) })).task;
|
|
607
|
+
}
|
|
608
|
+
/** Asks and waits until the task is done, failed, canceled or paused, however long (in rounds of up to a minute). */
|
|
609
|
+
async run(kind, input, options = {}) {
|
|
610
|
+
const task = await this.ask(kind, input, { ...options, wait: MAX_WAIT_S });
|
|
611
|
+
return this.settle(task.id, options.timeoutMs);
|
|
612
|
+
}
|
|
613
|
+
/** Asks, waits, and gives back the result itself; a task that fails, is canceled or waits for an answer throws a
|
|
614
|
+
* TaskError carrying the task. */
|
|
615
|
+
async result(kind, input, options = {}) {
|
|
616
|
+
const task = await this.run(kind, input, options);
|
|
617
|
+
if (task.status !== "done") throw new TaskError(task);
|
|
618
|
+
return task.result;
|
|
619
|
+
}
|
|
620
|
+
async get(id, wait = 0) {
|
|
621
|
+
return (await this.#client.call("GET", `/tasks/${id}${query({ wait: wait || void 0 })}`)).task;
|
|
622
|
+
}
|
|
623
|
+
/** Waits for a task to leave the line: done, failed, canceled or paused. */
|
|
624
|
+
async settle(id, timeoutMs = 10 * 6e4) {
|
|
625
|
+
const until = Date.now() + timeoutMs;
|
|
626
|
+
let task = await this.get(id);
|
|
627
|
+
while ((task.status === "queued" || task.status === "running") && Date.now() < until) task = await this.get(id, MAX_WAIT_S);
|
|
628
|
+
return task;
|
|
629
|
+
}
|
|
630
|
+
async list(options = {}) {
|
|
631
|
+
const tenant = options.tenant ?? this.#tenant;
|
|
632
|
+
return (await this.#client.call("GET", `/agents/${encodeURIComponent(this.#agent)}/tasks${query({ ...options, tenant })}`)).tasks;
|
|
633
|
+
}
|
|
634
|
+
/** What happened to a task after `after` (its events' seq); with `wait` seconds, waits for the first new one. */
|
|
635
|
+
async events(id, after = 0, wait = 0) {
|
|
636
|
+
return (await this.#client.call("GET", `/tasks/${id}/events${query({ after: after || void 0, wait: wait || void 0 })}`)).events;
|
|
637
|
+
}
|
|
638
|
+
/** What the agent did inside a task, step by step: its trace. */
|
|
639
|
+
async steps(id) {
|
|
640
|
+
return (await this.#client.call("GET", `/tasks/${id}/steps`)).steps;
|
|
641
|
+
}
|
|
642
|
+
async cancel(id) {
|
|
643
|
+
return (await this.#client.call("POST", `/tasks/${id}/cancel`, {})).task;
|
|
644
|
+
}
|
|
645
|
+
/** The questions waiting for an answer (a tenant's alone, in a tenant session). */
|
|
646
|
+
async pauses() {
|
|
647
|
+
return (await this.#client.call("GET", `/agents/${encodeURIComponent(this.#agent)}/pauses${query({ tenant: this.#tenant })}`)).pauses;
|
|
648
|
+
}
|
|
649
|
+
async answer(pauseId, answer) {
|
|
650
|
+
return (await this.#client.call("POST", `/pauses/${pauseId}/answer`, answer)).pause;
|
|
651
|
+
}
|
|
652
|
+
/** How a finished task went: what the agent learns from. A bad one also takes back what it did alone. */
|
|
653
|
+
async feedback(id, verdict, details = {}) {
|
|
654
|
+
return (await this.#client.call("POST", `/tasks/${id}/feedback`, { verdict, ...details })).feedback;
|
|
655
|
+
}
|
|
656
|
+
/** A task as it happens: each piece of its live answer (the agent's ctx.stream), each of its other events, and last the
|
|
657
|
+
* task itself once it is done, paused or failed. A stream that closes before the end is opened again after what
|
|
658
|
+
* was seen, so nothing comes twice. */
|
|
659
|
+
async *stream(id) {
|
|
660
|
+
let after = 0;
|
|
661
|
+
for (; ; ) {
|
|
662
|
+
for await (const sent of this.#client.events(`/tasks/${encodeURIComponent(id)}/stream`, after)) {
|
|
663
|
+
const parsed = JSON.parse(sent.data);
|
|
664
|
+
if (sent.event === "task") {
|
|
665
|
+
yield { type: "end", task: parsed };
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
after = sent.id || after;
|
|
669
|
+
const event = parsed;
|
|
670
|
+
const text = typeof event.data === "object" && event.data !== null && !Array.isArray(event.data) && typeof event.data.text === "string" ? event.data.text : "";
|
|
671
|
+
yield sent.event === "delta" ? { type: "delta", text, seq: event.seq } : { type: "event", event };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Asks for a task and gives its live answer as it is written: the pieces, then the task. */
|
|
676
|
+
async *live(kind, input, options = {}) {
|
|
677
|
+
const task = await this.ask(kind, input, { ...options, wait: 0 });
|
|
678
|
+
yield* this.stream(task.id);
|
|
679
|
+
}
|
|
680
|
+
/** Leaves a file in a session (a thread) for the agent's code to read: a price list, a catalogue, a document. Every
|
|
681
|
+
* task of that thread lists it (ctx.files) and the agent downloads it (ctx.file) while it works on one. The same
|
|
682
|
+
* name again replaces it. spredr keeps the bytes and never opens them; how the agent reads them is its code's. */
|
|
683
|
+
async attach(thread, name, data, options = {}) {
|
|
684
|
+
const tenant = options.tenant ?? this.#tenant;
|
|
685
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
686
|
+
const path = `/agents/${encodeURIComponent(this.#agent)}/files${query({ thread, file: name, ...tenant ? { tenant } : {} })}`;
|
|
687
|
+
return (await this.#client.upload(path, bytes, { "x-spredr-file-type": options.type ?? (typeof data === "string" ? "text/plain" : "application/octet-stream") })).file;
|
|
688
|
+
}
|
|
689
|
+
/** A session's files, without their bytes, newest first. */
|
|
690
|
+
async files(thread, options = {}) {
|
|
691
|
+
const tenant = options.tenant ?? this.#tenant;
|
|
692
|
+
return (await this.#client.call("GET", `/agents/${encodeURIComponent(this.#agent)}/files${query({ thread, ...tenant ? { tenant } : {} })}`)).files;
|
|
693
|
+
}
|
|
694
|
+
/** Takes a file out of a session. */
|
|
695
|
+
async detach(thread, name, options = {}) {
|
|
696
|
+
const tenant = options.tenant ?? this.#tenant;
|
|
697
|
+
await this.#client.call("DELETE", `/agents/${encodeURIComponent(this.#agent)}/files${query({ thread, file: name, ...tenant ? { tenant } : {} })}`);
|
|
698
|
+
}
|
|
699
|
+
/** The client and agent a tenant session works with. */
|
|
700
|
+
get base() {
|
|
701
|
+
return { client: this.#client, agent: this.#agent };
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
var Tenant = class extends Tasks {
|
|
705
|
+
name;
|
|
706
|
+
#path;
|
|
707
|
+
constructor(client2, agent, tenant) {
|
|
708
|
+
super(client2, agent, tenant);
|
|
709
|
+
this.name = tenant;
|
|
710
|
+
this.#path = `/agents/${encodeURIComponent(agent)}/tenants/${encodeURIComponent(tenant)}`;
|
|
711
|
+
}
|
|
712
|
+
/** What it used today and this month, its caps, the facts it holds and its open questions. */
|
|
713
|
+
async usage() {
|
|
714
|
+
return (await this.base.client.call("GET", this.#path)).tenant;
|
|
715
|
+
}
|
|
716
|
+
/** Caps this tenant on top of the agent's own; null lifts one. */
|
|
717
|
+
async setLimits(limits) {
|
|
718
|
+
return (await this.base.client.call("PUT", `${this.#path}/limits`, limits)).caps;
|
|
719
|
+
}
|
|
720
|
+
/** Forgets this tenant whole: its tasks, memory, questions, freedom, lessons and caps. The others stay. */
|
|
721
|
+
async forget() {
|
|
722
|
+
return (await this.base.client.call("DELETE", this.#path)).forgotten;
|
|
723
|
+
}
|
|
724
|
+
/** How free the agent is with each action for this tenant. */
|
|
725
|
+
async freedom() {
|
|
726
|
+
return (await this.base.client.call("GET", `/agents/${encodeURIComponent(this.base.agent)}/freedom${query({ tenant: this.name })}`)).freedom;
|
|
727
|
+
}
|
|
728
|
+
/** Lets an action run alone for this tenant (auto), or ask every time (ask): never for another tenant. */
|
|
729
|
+
async setFreedom(action, level) {
|
|
730
|
+
return (await this.base.client.call("PUT", `/agents/${encodeURIComponent(this.base.agent)}/freedom/${encodeURIComponent(action)}`, { level, tenant: this.name })).freedom;
|
|
731
|
+
}
|
|
732
|
+
/** This tenant's lessons: its own rules, in every one of its tasks from the moment they are written. */
|
|
733
|
+
async lessons() {
|
|
734
|
+
return (await this.base.client.call("GET", `/agents/${encodeURIComponent(this.base.agent)}/lessons${query({ tenant: this.name })}`)).lessons;
|
|
735
|
+
}
|
|
736
|
+
/** Teaches the agent a rule for this tenant alone («we never book on Sundays»). */
|
|
737
|
+
async teach(text, why = "") {
|
|
738
|
+
return (await this.base.client.call("POST", `/agents/${encodeURIComponent(this.base.agent)}/lessons`, { text, why, tenant: this.name })).lesson;
|
|
739
|
+
}
|
|
740
|
+
/** Accepts or refuses a lesson the agent proposed for this tenant, or retires one. */
|
|
741
|
+
async decide(lessonId, decision) {
|
|
742
|
+
return (await this.base.client.call("POST", `/agents/${encodeURIComponent(this.base.agent)}/lessons/${lessonId}`, { decision })).lesson;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/caller.ts
|
|
747
|
+
function need2(value, name) {
|
|
748
|
+
const found = value || process.env[name];
|
|
749
|
+
if (!found) throw new Error(`${name} is not set: link this backend to its agent in spredr (it writes the variables), or pass the options.`);
|
|
750
|
+
return found;
|
|
751
|
+
}
|
|
752
|
+
var Caller = class _Caller extends Tasks {
|
|
753
|
+
#id;
|
|
754
|
+
#verify;
|
|
755
|
+
constructor(options = {}) {
|
|
756
|
+
const { id, key, agent, ai, aiKey, ...clientOptions } = options;
|
|
757
|
+
super(client(need2(ai, "SPREDR_AI_URL"), { from: need2(id, "SPREDR_CALLER_ID"), key: privateKeyOf(need2(key, "SPREDR_CALLER_KEY")) }, clientOptions), need2(agent, "SPREDR_AGENT"));
|
|
758
|
+
this.#id = need2(id, "SPREDR_CALLER_ID");
|
|
759
|
+
const checkWith = aiKey || process.env.SPREDR_AI_KEY;
|
|
760
|
+
this.#verify = checkWith ? aiVerifier(checkWith) : null;
|
|
761
|
+
}
|
|
762
|
+
/** The same as `new Caller(overrides)`: from the variables spredr writes into a linked service. */
|
|
763
|
+
static fromEnv(overrides = {}) {
|
|
764
|
+
return new _Caller(overrides);
|
|
765
|
+
}
|
|
766
|
+
/** The callback, when it really comes from ai (signed, recent, once); null otherwise. Give it the raw body. */
|
|
767
|
+
callback(request) {
|
|
768
|
+
if (!this.#verify) throw new Error("Pass aiKey (SPREDR_AI_KEY) to check callbacks.");
|
|
769
|
+
if (!this.#verify(request)) return null;
|
|
770
|
+
const text = typeof request.body === "string" ? request.body : Buffer.from(request.body).toString("utf8");
|
|
771
|
+
const parsed = JSON.parse(text);
|
|
772
|
+
if (typeof parsed !== "object" || parsed === null || !("event" in parsed) || !("task" in parsed)) return null;
|
|
773
|
+
const to = "to" in parsed && typeof parsed.to === "object" && parsed.to !== null && "caller" in parsed.to ? parsed.to.caller : void 0;
|
|
774
|
+
if (to !== void 0 && to !== this.#id) return null;
|
|
775
|
+
return parsed;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/owner.ts
|
|
780
|
+
var DEFAULT_AI_URL = "https://api.spredr.cloud/ai";
|
|
781
|
+
var Spredr = class {
|
|
782
|
+
#client;
|
|
783
|
+
constructor(options = {}) {
|
|
784
|
+
const { token: given, ai, ...clientOptions } = options;
|
|
785
|
+
const token = given || process.env.SPREDR_TOKEN;
|
|
786
|
+
if (!token) throw new Error("SPREDR_TOKEN is not set: make an API token in the panel (Account \u203A API tokens), or pass { token }.");
|
|
787
|
+
this.#client = client(ai || process.env.SPREDR_AI_URL || DEFAULT_AI_URL, { token }, clientOptions);
|
|
788
|
+
}
|
|
789
|
+
async agents() {
|
|
790
|
+
return (await this.#client.call("GET", "/agents")).agents;
|
|
791
|
+
}
|
|
792
|
+
/** Turns a spredr service into an agent: ai writes its keys into the service and deploys it again. A deploy spredr
|
|
793
|
+
* refuses (redeploy.status «failed») leaves the agent made: deploy the service again once that is fixed. */
|
|
794
|
+
async register(name, service, options = {}) {
|
|
795
|
+
const made = await this.#client.call("POST", "/agents", { name, service, ...options });
|
|
796
|
+
return { ...made.agent, redeploy: made.redeploy };
|
|
797
|
+
}
|
|
798
|
+
/** Leaves a token for ai to wake and put to sleep your agents while nobody is looking. */
|
|
799
|
+
async keepToken(token) {
|
|
800
|
+
await this.#client.call("PUT", "/me/key", { token });
|
|
801
|
+
}
|
|
802
|
+
/** What waits for a person across every agent: open questions, tasks that failed in the last `hours`, and agents whose
|
|
803
|
+
* memory is 80 % full or more. */
|
|
804
|
+
async inbox(hours = 24) {
|
|
805
|
+
return this.#client.call("GET", `/inbox${query({ hours })}`);
|
|
806
|
+
}
|
|
807
|
+
agent(name) {
|
|
808
|
+
return new OwnedAgent(this.#client, name);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
var OwnedAgent = class {
|
|
812
|
+
#client;
|
|
813
|
+
#path;
|
|
814
|
+
tasks;
|
|
815
|
+
constructor(client2, name) {
|
|
816
|
+
this.#client = client2;
|
|
817
|
+
this.#path = `/agents/${encodeURIComponent(name)}`;
|
|
818
|
+
this.tasks = new Tasks(client2, name);
|
|
819
|
+
}
|
|
820
|
+
/** The agent working for one tenant: its tasks, usage, caps, lessons and freedom, and forgetting it. */
|
|
821
|
+
for(tenant) {
|
|
822
|
+
return this.tasks.for(tenant);
|
|
823
|
+
}
|
|
824
|
+
/** The tenants it worked for in the last `days`, the latest active first, each with what it used. */
|
|
825
|
+
async tenants(options = {}) {
|
|
826
|
+
return (await this.#client.call("GET", `${this.#path}/tenants${query(options)}`)).tenants;
|
|
827
|
+
}
|
|
828
|
+
/** The caps of every tenant without caps of its own. */
|
|
829
|
+
async setTenantLimits(limits) {
|
|
830
|
+
return (await this.#client.call("PUT", `${this.#path}/tenants/*/limits`, limits)).caps;
|
|
831
|
+
}
|
|
832
|
+
async info() {
|
|
833
|
+
return (await this.#client.call("GET", this.#path)).agent;
|
|
834
|
+
}
|
|
835
|
+
/** Its last hours at a glance: tasks, failures, times, tokens, cost, tools, and how free it is. */
|
|
836
|
+
async summary(hours = 24) {
|
|
837
|
+
return (await this.#client.call("GET", `${this.#path}/summary${query({ hours })}`)).summary;
|
|
838
|
+
}
|
|
839
|
+
/** Its conversations: tasks that share a thread. Read one with tasks.list({ thread }). */
|
|
840
|
+
async threads(limit = 50) {
|
|
841
|
+
return (await this.#client.call("GET", `${this.#path}/threads${query({ limit })}`)).threads;
|
|
842
|
+
}
|
|
843
|
+
/** Every task since a moment with its steps (the export, read line by line). */
|
|
844
|
+
async export(since) {
|
|
845
|
+
const text = await this.#client.text("GET", `${this.#path}/export${query({ since: since ? new Date(since).toISOString() : void 0 })}`);
|
|
846
|
+
return text.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
847
|
+
}
|
|
848
|
+
async setLimits(limits) {
|
|
849
|
+
return (await this.#client.call("PUT", `${this.#path}/limits`, limits)).agent;
|
|
850
|
+
}
|
|
851
|
+
/** Sends each finished task to your OpenTelemetry endpoint (OTLP/HTTP: Langfuse, Datadog, Honeycomb, your collector)
|
|
852
|
+
* as a trace. headers carry its key (sealed by ai, never shown back); withContent sends the input and result too. */
|
|
853
|
+
async setTraces(traces) {
|
|
854
|
+
return (await this.#client.call("PUT", `${this.#path}/traces`, traces)).traces;
|
|
855
|
+
}
|
|
856
|
+
/** Where its traces go (the header names only), or null. */
|
|
857
|
+
async traces() {
|
|
858
|
+
return (await this.#client.call("GET", `${this.#path}/traces`)).traces;
|
|
859
|
+
}
|
|
860
|
+
async stopTraces() {
|
|
861
|
+
await this.#client.call("DELETE", `${this.#path}/traces`);
|
|
862
|
+
}
|
|
863
|
+
async remove() {
|
|
864
|
+
await this.#client.call("DELETE", this.#path);
|
|
865
|
+
}
|
|
866
|
+
/** A new key pair: ai believes the new key at once, whatever the redeploy does (redeploy.status says). */
|
|
867
|
+
async rotateKey(options = {}) {
|
|
868
|
+
const rotated = await this.#client.call("POST", `${this.#path}/rotate`, options);
|
|
869
|
+
return { ...rotated.agent, redeploy: rotated.redeploy };
|
|
870
|
+
}
|
|
871
|
+
/** A backend that may give this agent tasks. With `service`, ai writes its key there; with `publicKey`, the private
|
|
872
|
+
* one never leaves you; with neither, the private key comes back this once. With `everything` and a `callbackUrl`, it
|
|
873
|
+
* is an events webhook: it hears about every task of the agent (task.created, done, failed, paused, canceled). */
|
|
874
|
+
async addCaller(name, options = {}) {
|
|
875
|
+
return this.#client.call("POST", `${this.#path}/callers`, { name, ...options });
|
|
876
|
+
}
|
|
877
|
+
async callers() {
|
|
878
|
+
return (await this.#client.call("GET", `${this.#path}/callers`)).callers;
|
|
879
|
+
}
|
|
880
|
+
async removeCaller(id) {
|
|
881
|
+
await this.#client.call("DELETE", `${this.#path}/callers/${id}`);
|
|
882
|
+
}
|
|
883
|
+
async schedule(name, kind, cron, options = {}) {
|
|
884
|
+
return (await this.#client.call("POST", `${this.#path}/schedules`, { name, kind, cron, ...options })).schedule;
|
|
885
|
+
}
|
|
886
|
+
/** A secret address that starts a task with whatever is POSTed to it; the address comes back this once. */
|
|
887
|
+
async hook(name, kind) {
|
|
888
|
+
return this.#client.call("POST", `${this.#path}/hooks`, { name, kind });
|
|
889
|
+
}
|
|
890
|
+
/** Every file its sessions keep, newest first (each with its tenant, conversation and place in the bucket), and how full
|
|
891
|
+
* its room for them is. */
|
|
892
|
+
async storedFiles(options = {}) {
|
|
893
|
+
return this.#client.call("GET", `${this.#path}/stored-files${query(options)}`);
|
|
894
|
+
}
|
|
895
|
+
/** One of them, as its bytes and type: any session's. */
|
|
896
|
+
async downloadFile(id) {
|
|
897
|
+
const got = await this.#client.download(`${this.#path}/stored-files/${encodeURIComponent(id)}`);
|
|
898
|
+
return { data: got.data, type: got.type };
|
|
899
|
+
}
|
|
900
|
+
/** Removes one, any session's: its card and its bytes. */
|
|
901
|
+
async deleteFile(id) {
|
|
902
|
+
await this.#client.call("DELETE", `${this.#path}/stored-files/${encodeURIComponent(id)}`);
|
|
903
|
+
}
|
|
904
|
+
/** How full its memory is (warn from 80 %, full from 95 %), and whether unchanged facts are forgotten. */
|
|
905
|
+
async memoryUse() {
|
|
906
|
+
return (await this.#client.call("GET", `${this.#path}/memory/use`)).use;
|
|
907
|
+
}
|
|
908
|
+
/** What it remembers; `tenant` narrows it to one tenant's. */
|
|
909
|
+
async memory(options = {}) {
|
|
910
|
+
return (await this.#client.call("GET", `${this.#path}/memory${query(options)}`)).facts;
|
|
911
|
+
}
|
|
912
|
+
async editFact(id, text, data) {
|
|
913
|
+
return (await this.#client.call("PATCH", `${this.#path}/memory/${id}`, { text, ...data === void 0 ? {} : { data } })).fact;
|
|
914
|
+
}
|
|
915
|
+
/** Erases everything the agent keeps under a scope (a customer who asks to be forgotten): the agent's own, or one
|
|
916
|
+
* tenant's. */
|
|
917
|
+
async forget(scope, tenant) {
|
|
918
|
+
return (await this.#client.call("POST", `${this.#path}/memory/forget`, { scope, ...tenant ? { tenant } : {} })).forgotten;
|
|
919
|
+
}
|
|
920
|
+
/** How free it is with each action in its own work (no tenant); a tenant's is `for(tenant).freedom()`. */
|
|
921
|
+
async freedom() {
|
|
922
|
+
return (await this.#client.call("GET", `${this.#path}/freedom`)).freedom;
|
|
923
|
+
}
|
|
924
|
+
async setFreedom(action, level) {
|
|
925
|
+
return (await this.#client.call("PUT", `${this.#path}/freedom/${encodeURIComponent(action)}`, { level })).freedom;
|
|
926
|
+
}
|
|
927
|
+
async lessons() {
|
|
928
|
+
return (await this.#client.call("GET", `${this.#path}/lessons`)).lessons;
|
|
929
|
+
}
|
|
930
|
+
async writeLesson(text, why = "") {
|
|
931
|
+
return (await this.#client.call("POST", `${this.#path}/lessons`, { text, why })).lesson;
|
|
932
|
+
}
|
|
933
|
+
async decide(lessonId, decision) {
|
|
934
|
+
return (await this.#client.call("POST", `${this.#path}/lessons/${lessonId}`, { decision })).lesson;
|
|
935
|
+
}
|
|
936
|
+
async versions() {
|
|
937
|
+
return (await this.#client.call("GET", `${this.#path}/versions`)).versions;
|
|
938
|
+
}
|
|
939
|
+
/** Releases the accepted lessons as a new version, tried first on past tasks. */
|
|
940
|
+
async release() {
|
|
941
|
+
return (await this.#client.call("POST", `${this.#path}/versions`, {})).version;
|
|
942
|
+
}
|
|
943
|
+
async version(version, action) {
|
|
944
|
+
return (await this.#client.call("POST", `${this.#path}/versions/${version}`, { action })).version;
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
export {
|
|
948
|
+
Agent,
|
|
949
|
+
AuthenticationError,
|
|
950
|
+
BadRequestError,
|
|
951
|
+
Caller,
|
|
952
|
+
ConflictError,
|
|
953
|
+
ConnectionError,
|
|
954
|
+
DEFAULT_AI_URL,
|
|
955
|
+
InternalServerError,
|
|
956
|
+
NotFoundError,
|
|
957
|
+
OwnedAgent,
|
|
958
|
+
PermissionDeniedError,
|
|
959
|
+
RateLimitError,
|
|
960
|
+
Spredr,
|
|
961
|
+
SpredrError,
|
|
962
|
+
TaskError,
|
|
963
|
+
Tasks,
|
|
964
|
+
Tenant,
|
|
965
|
+
TimeoutError,
|
|
966
|
+
aiVerifier,
|
|
967
|
+
currentTask,
|
|
968
|
+
newKeys
|
|
969
|
+
};
|