broods 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.md +105 -0
- package/README.md +41 -0
- package/dist/cli/index.js +3204 -0
- package/dist/index.d.ts +1008 -0
- package/dist/index.js +845 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/run-input.ts
|
|
3
|
+
function resolveRunEvents(input) {
|
|
4
|
+
if (input.events && input.input !== undefined) {
|
|
5
|
+
throw new Error("Run input accepts either `input` or `events`, not both");
|
|
6
|
+
}
|
|
7
|
+
if (input.events && input.events.length > 0)
|
|
8
|
+
return input.events;
|
|
9
|
+
if (typeof input.input === "string") {
|
|
10
|
+
return [{ role: "user", content: [{ type: "text", text: input.input }] }];
|
|
11
|
+
}
|
|
12
|
+
throw new Error("Run input requires `input` (string) or a non-empty `events` array");
|
|
13
|
+
}
|
|
14
|
+
// src/stream.ts
|
|
15
|
+
async function* readSseStream(body) {
|
|
16
|
+
const reader = body.getReader();
|
|
17
|
+
const decoder = new TextDecoder;
|
|
18
|
+
let buffer = "";
|
|
19
|
+
try {
|
|
20
|
+
while (true) {
|
|
21
|
+
const { done, value } = await reader.read();
|
|
22
|
+
if (done)
|
|
23
|
+
break;
|
|
24
|
+
buffer += decoder.decode(value, { stream: true });
|
|
25
|
+
const lines = buffer.split(`
|
|
26
|
+
`);
|
|
27
|
+
buffer = lines.pop() ?? "";
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (line.startsWith("data: "))
|
|
30
|
+
yield line.slice(6);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
reader.releaseLock();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// src/observability-contracts.ts
|
|
38
|
+
var MAX_OBSERVABILITY_BACKFILL = 500;
|
|
39
|
+
function isObservabilityClientMessage(v) {
|
|
40
|
+
if (typeof v !== "object" || v === null)
|
|
41
|
+
return false;
|
|
42
|
+
const msg = v;
|
|
43
|
+
if (msg["type"] === "subscribe") {
|
|
44
|
+
const stream = msg["stream"];
|
|
45
|
+
if (stream !== "logs" && stream !== "traces")
|
|
46
|
+
return false;
|
|
47
|
+
const backfill = msg["backfill"];
|
|
48
|
+
if (backfill !== undefined && (typeof backfill !== "number" || !Number.isSafeInteger(backfill) || backfill < 0 || backfill > MAX_OBSERVABILITY_BACKFILL))
|
|
49
|
+
return false;
|
|
50
|
+
const minLevel = msg["minLevel"];
|
|
51
|
+
if (minLevel !== undefined && minLevel !== "INFO" && minLevel !== "WARN" && minLevel !== "ERROR")
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (msg["type"] === "unsubscribe") {
|
|
56
|
+
const stream = msg["stream"];
|
|
57
|
+
return stream === "logs" || stream === "traces";
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// src/runtime-config.ts
|
|
62
|
+
import { existsSync, readFileSync } from "fs";
|
|
63
|
+
import { join as join2, resolve } from "path";
|
|
64
|
+
|
|
65
|
+
// src/config.ts
|
|
66
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
67
|
+
import { homedir } from "os";
|
|
68
|
+
import { dirname, join } from "path";
|
|
69
|
+
var PROJECT_DIR = "broods";
|
|
70
|
+
var GENERATED_DIR = "_generated";
|
|
71
|
+
var USER_CONFIG_PATH = join(homedir(), ".broods", "config.json");
|
|
72
|
+
async function readStoredAuth() {
|
|
73
|
+
const envToken = process.env.BROODS_TOKEN;
|
|
74
|
+
const envUrl = process.env.BROODS_DASHBOARD_URL;
|
|
75
|
+
if (envToken && envUrl) {
|
|
76
|
+
return {
|
|
77
|
+
dashboardUrl: envUrl,
|
|
78
|
+
token: envToken,
|
|
79
|
+
createdAt: new Date().toISOString()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(await readFile(USER_CONFIG_PATH, "utf8"));
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function writeStoredAuth(config) {
|
|
89
|
+
await mkdir(dirname(USER_CONFIG_PATH), { recursive: true });
|
|
90
|
+
await writeFile(USER_CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
91
|
+
`, "utf8");
|
|
92
|
+
}
|
|
93
|
+
function stripTrailingSlash(value) {
|
|
94
|
+
return value.replace(/\/+$/, "");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/runtime-config.ts
|
|
98
|
+
var loadedEnvForCwd = null;
|
|
99
|
+
function loadBroodsRuntimeConfig(cwd = process.cwd()) {
|
|
100
|
+
loadEnvFiles(cwd);
|
|
101
|
+
const stored = readStoredAuthSync();
|
|
102
|
+
return {
|
|
103
|
+
dashboardUrl: process.env.BROODS_DASHBOARD_URL ?? stored?.dashboardUrl,
|
|
104
|
+
token: process.env.BROODS_TOKEN ?? stored?.token,
|
|
105
|
+
project: process.env.BROODS_PROJECT,
|
|
106
|
+
environment: process.env.BROODS_ENVIRONMENT
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function loadEnvFiles(cwd) {
|
|
110
|
+
const root = resolve(cwd);
|
|
111
|
+
if (loadedEnvForCwd === root)
|
|
112
|
+
return;
|
|
113
|
+
loadedEnvForCwd = root;
|
|
114
|
+
const originallySet = new Set(Object.keys(process.env));
|
|
115
|
+
for (const file of [".env", ".env.local"]) {
|
|
116
|
+
const path = join2(root, file);
|
|
117
|
+
if (!existsSync(path))
|
|
118
|
+
continue;
|
|
119
|
+
for (const [key, value] of Object.entries(parseEnv(readFileSync(path, "utf8")))) {
|
|
120
|
+
if (!originallySet.has(key))
|
|
121
|
+
process.env[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function parseEnv(source) {
|
|
126
|
+
const values = {};
|
|
127
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
128
|
+
const line = rawLine.trim();
|
|
129
|
+
if (!line || line.startsWith("#"))
|
|
130
|
+
continue;
|
|
131
|
+
const assignment = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
132
|
+
const eq = assignment.indexOf("=");
|
|
133
|
+
if (eq <= 0)
|
|
134
|
+
continue;
|
|
135
|
+
const key = assignment.slice(0, eq).trim();
|
|
136
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
137
|
+
continue;
|
|
138
|
+
values[key] = unquoteEnvValue(assignment.slice(eq + 1).trim());
|
|
139
|
+
}
|
|
140
|
+
return values;
|
|
141
|
+
}
|
|
142
|
+
function unquoteEnvValue(value) {
|
|
143
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
144
|
+
return value.slice(1, -1).replace(/\\n/g, `
|
|
145
|
+
`).replace(/\\"/g, '"');
|
|
146
|
+
}
|
|
147
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
148
|
+
return value.slice(1, -1);
|
|
149
|
+
}
|
|
150
|
+
const commentIndex = value.indexOf(" #");
|
|
151
|
+
return commentIndex >= 0 ? value.slice(0, commentIndex).trimEnd() : value;
|
|
152
|
+
}
|
|
153
|
+
function readStoredAuthSync() {
|
|
154
|
+
try {
|
|
155
|
+
const value = JSON.parse(readFileSync(USER_CONFIG_PATH, "utf8"));
|
|
156
|
+
if (typeof value.dashboardUrl !== "string" || typeof value.token !== "string")
|
|
157
|
+
return null;
|
|
158
|
+
return {
|
|
159
|
+
dashboardUrl: stripTrailingSlash(value.dashboardUrl),
|
|
160
|
+
token: value.token
|
|
161
|
+
};
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/client.ts
|
|
168
|
+
var DEFAULT_CORE_BASE_URL = "https://gateway.broods.app";
|
|
169
|
+
|
|
170
|
+
class BroodsClient {
|
|
171
|
+
baseUrl;
|
|
172
|
+
apiKey;
|
|
173
|
+
fetchImpl;
|
|
174
|
+
constructor(options = {}) {
|
|
175
|
+
loadBroodsRuntimeConfig();
|
|
176
|
+
this.baseUrl = normalizeHttpServiceUrl(options.baseUrl || options.host || process.env.BROODS_BASE_URL || process.env.BROODS_HOST || DEFAULT_CORE_BASE_URL);
|
|
177
|
+
this.apiKey = options.apiKey || process.env.BROODS_API_KEY || undefined;
|
|
178
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
179
|
+
}
|
|
180
|
+
channelWebhookUrl(ref) {
|
|
181
|
+
return `${this.baseUrl}${ref.webhookPath.startsWith("/") ? "" : "/"}${ref.webhookPath}`;
|
|
182
|
+
}
|
|
183
|
+
agent(refOrName, agentId) {
|
|
184
|
+
if (typeof refOrName === "string") {
|
|
185
|
+
const name = refOrName;
|
|
186
|
+
const id = agentId ?? "";
|
|
187
|
+
if (!id)
|
|
188
|
+
throw new Error(`Agent ${name} is missing a generated id. Run broods deploy first.`);
|
|
189
|
+
return {
|
|
190
|
+
id,
|
|
191
|
+
run: (input) => this.run({ ...input, agentName: name, agentId: id }),
|
|
192
|
+
runAsync: (input) => this.runAsync({ ...input, agentName: name, agentId: id }),
|
|
193
|
+
stream: (input) => this.stream({ ...input, agentName: name, agentId: id })
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const ref = refOrName;
|
|
197
|
+
if (!ref.id)
|
|
198
|
+
throw new Error(`Agent ${ref.name} is missing a generated id. Run broods deploy first.`);
|
|
199
|
+
return {
|
|
200
|
+
id: ref.id,
|
|
201
|
+
run: (input) => this.run(ref, input),
|
|
202
|
+
runAsync: (input) => this.runAsync(ref, input),
|
|
203
|
+
stream: (input) => this.stream(ref, input)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async run(refOrInput, maybeInput) {
|
|
207
|
+
const events = [];
|
|
208
|
+
let text = "";
|
|
209
|
+
const stream = maybeInput ? this.stream(refOrInput, maybeInput) : this.stream(refOrInput);
|
|
210
|
+
for await (const part of stream) {
|
|
211
|
+
events.push(part);
|
|
212
|
+
if (part.type === "text-delta")
|
|
213
|
+
text += part.text;
|
|
214
|
+
}
|
|
215
|
+
return { text, events };
|
|
216
|
+
}
|
|
217
|
+
async* stream(refOrInput, maybeInput) {
|
|
218
|
+
const input = maybeInput ? {
|
|
219
|
+
...maybeInput,
|
|
220
|
+
agentId: refOrInput.id,
|
|
221
|
+
agentName: refOrInput.name
|
|
222
|
+
} : refOrInput;
|
|
223
|
+
const body = directRunBody(input, "cli");
|
|
224
|
+
const targetUrl = maybeInput ? this.scopedUrl(refOrInput) : this.baseUrl;
|
|
225
|
+
const response = await this.openStream(body, targetUrl);
|
|
226
|
+
if (!response.ok)
|
|
227
|
+
throw new Error(`Run failed: ${response.status} ${await response.text()}`);
|
|
228
|
+
if (!response.body)
|
|
229
|
+
throw new Error("Run response has no body");
|
|
230
|
+
for await (const data of readSseStream(response.body)) {
|
|
231
|
+
let part;
|
|
232
|
+
try {
|
|
233
|
+
part = JSON.parse(data);
|
|
234
|
+
} catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (part.type === "error")
|
|
238
|
+
throw new Error(`Agent run failed: ${formatStreamError(part.error)}`);
|
|
239
|
+
yield part;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async runAsync(refOrInput, maybeInput) {
|
|
243
|
+
const input = maybeInput ? {
|
|
244
|
+
...maybeInput,
|
|
245
|
+
agentId: refOrInput.id,
|
|
246
|
+
agentName: refOrInput.name
|
|
247
|
+
} : refOrInput;
|
|
248
|
+
const body = directRunBody(input, "async");
|
|
249
|
+
const response = await this.fetchJson(`${this.baseUrl}/async`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: this.apiKeyHeaders(),
|
|
252
|
+
body: JSON.stringify(body)
|
|
253
|
+
});
|
|
254
|
+
if (response.status !== 202) {
|
|
255
|
+
throw new Error(`Async run failed: ${response.status} ${await responseErrorDetails(response, "202 JSON")}`);
|
|
256
|
+
}
|
|
257
|
+
const accepted = normalizeAsyncAccepted(await response.json(), body);
|
|
258
|
+
return {
|
|
259
|
+
...accepted,
|
|
260
|
+
conversationKey: body.conversationKey,
|
|
261
|
+
poll: () => this.getAsyncStatus(accepted),
|
|
262
|
+
wait: (options) => this.waitForAsyncStatus(accepted, options)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async getAsyncStatus(status, options = {}) {
|
|
266
|
+
const statusUrl = this.resolveStatusUrl(status, options);
|
|
267
|
+
const response = await this.fetchJson(statusUrl, {
|
|
268
|
+
method: "GET",
|
|
269
|
+
headers: this.apiKeyHeaders()
|
|
270
|
+
});
|
|
271
|
+
if (response.status === 404)
|
|
272
|
+
return { status: "not_found" };
|
|
273
|
+
if (!response.ok)
|
|
274
|
+
throw new Error(`Status check failed: ${response.status} ${await response.text()}`);
|
|
275
|
+
return await response.json();
|
|
276
|
+
}
|
|
277
|
+
async waitForAsyncStatus(status, options = {}) {
|
|
278
|
+
const deadline = Date.now() + (options.timeoutMs ?? 180000);
|
|
279
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
280
|
+
while (Date.now() < deadline) {
|
|
281
|
+
if (options.signal?.aborted)
|
|
282
|
+
throw new Error("Async status polling aborted.");
|
|
283
|
+
const payload = await this.getAsyncStatus(status, options);
|
|
284
|
+
if (payload.status === "awaiting_approval" || payload.status === "completed" || payload.status === "failed" || payload.status === "not_found") {
|
|
285
|
+
return payload;
|
|
286
|
+
}
|
|
287
|
+
await sleep(intervalMs, options.signal);
|
|
288
|
+
}
|
|
289
|
+
throw new Error("Polling timeout");
|
|
290
|
+
}
|
|
291
|
+
async createCron(input) {
|
|
292
|
+
const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: this.apiKeyHeaders(),
|
|
295
|
+
body: JSON.stringify(resolveCronInput(input))
|
|
296
|
+
});
|
|
297
|
+
if (response.status !== 201)
|
|
298
|
+
throw new Error(`Create cron job failed: ${response.status} ${await cronErrorDetails(response)}`);
|
|
299
|
+
return await response.json();
|
|
300
|
+
}
|
|
301
|
+
async listCrons() {
|
|
302
|
+
const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons`, {
|
|
303
|
+
method: "GET",
|
|
304
|
+
headers: this.apiKeyHeaders()
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok)
|
|
307
|
+
throw new Error(`List cron jobs failed: ${response.status} ${await cronErrorDetails(response)}`);
|
|
308
|
+
const payload = await response.json();
|
|
309
|
+
return payload.crons;
|
|
310
|
+
}
|
|
311
|
+
async getCron(cronId) {
|
|
312
|
+
const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
|
|
313
|
+
method: "GET",
|
|
314
|
+
headers: this.apiKeyHeaders()
|
|
315
|
+
});
|
|
316
|
+
if (response.status === 404)
|
|
317
|
+
return null;
|
|
318
|
+
if (!response.ok)
|
|
319
|
+
throw new Error(`Get cron job failed: ${response.status} ${await cronErrorDetails(response)}`);
|
|
320
|
+
return await response.json();
|
|
321
|
+
}
|
|
322
|
+
async listCronRuns(cronId, options = {}) {
|
|
323
|
+
const params = new URLSearchParams;
|
|
324
|
+
if (options.limit !== undefined)
|
|
325
|
+
params.set("limit", String(options.limit));
|
|
326
|
+
const suffix = params.size > 0 ? `?${params}` : "";
|
|
327
|
+
const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}/runs${suffix}`, {
|
|
328
|
+
method: "GET",
|
|
329
|
+
headers: this.apiKeyHeaders()
|
|
330
|
+
});
|
|
331
|
+
if (!response.ok)
|
|
332
|
+
throw new Error(`List cron job runs failed: ${response.status} ${await cronErrorDetails(response)}`);
|
|
333
|
+
const payload = await response.json();
|
|
334
|
+
return payload.runs;
|
|
335
|
+
}
|
|
336
|
+
async updateCron(cronId, patch) {
|
|
337
|
+
const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
|
|
338
|
+
method: "PATCH",
|
|
339
|
+
headers: this.apiKeyHeaders(),
|
|
340
|
+
body: JSON.stringify(patch)
|
|
341
|
+
});
|
|
342
|
+
if (!response.ok)
|
|
343
|
+
throw new Error(`Update cron job failed: ${response.status} ${await cronErrorDetails(response)}`);
|
|
344
|
+
return await response.json();
|
|
345
|
+
}
|
|
346
|
+
async deleteCron(cronId) {
|
|
347
|
+
const response = await this.fetchJson(`${this.baseUrl}/accounts/me/crons/${encodeURIComponent(cronId)}`, {
|
|
348
|
+
method: "DELETE",
|
|
349
|
+
headers: this.apiKeyHeaders()
|
|
350
|
+
});
|
|
351
|
+
if (response.status === 404)
|
|
352
|
+
return false;
|
|
353
|
+
if (!response.ok)
|
|
354
|
+
throw new Error(`Delete cron job failed: ${response.status} ${await cronErrorDetails(response)}`);
|
|
355
|
+
const payload = await response.json();
|
|
356
|
+
return payload.deleted;
|
|
357
|
+
}
|
|
358
|
+
scopedUrl(ref) {
|
|
359
|
+
if (ref.projectSlug && ref.environmentSlug && ref.endpointId) {
|
|
360
|
+
return `${this.baseUrl}/v1/${encodeURIComponent(ref.projectSlug)}` + `/agents/${encodeURIComponent(ref.environmentSlug)}/${encodeURIComponent(ref.endpointId)}`;
|
|
361
|
+
}
|
|
362
|
+
return this.baseUrl;
|
|
363
|
+
}
|
|
364
|
+
async openStream(body, targetUrl) {
|
|
365
|
+
if (this.apiKey) {
|
|
366
|
+
return await this.fetchCore(body, targetUrl, {
|
|
367
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
throw new Error(`BroodsClient streams directly from the core service at ${this.baseUrl}. ` + "Provide apiKey. " + "For a self-hosted core service, set host/baseUrl or BROODS_HOST/BROODS_BASE_URL.");
|
|
371
|
+
}
|
|
372
|
+
async fetchCore(body, targetUrl, authHeaders) {
|
|
373
|
+
try {
|
|
374
|
+
return await this.fetchImpl(targetUrl, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json",
|
|
378
|
+
Accept: "text/event-stream",
|
|
379
|
+
...authHeaders
|
|
380
|
+
},
|
|
381
|
+
body: JSON.stringify(body)
|
|
382
|
+
});
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new Error(`Cannot access the broods core service at ${targetUrl}. ` + `The SDK uses ${DEFAULT_CORE_BASE_URL} by default; set host/baseUrl or BROODS_HOST/BROODS_BASE_URL ` + `to your own core service URL if your account uses a custom deployment. ` + `Cause: ${error instanceof Error ? error.message : String(error)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
apiKeyHeaders() {
|
|
388
|
+
if (!this.apiKey) {
|
|
389
|
+
throw new Error("BroodsClient requires apiKey or BROODS_API_KEY.");
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
"Content-Type": "application/json",
|
|
393
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async fetchJson(targetUrl, init) {
|
|
397
|
+
try {
|
|
398
|
+
return await this.fetchImpl(targetUrl, {
|
|
399
|
+
...init,
|
|
400
|
+
headers: {
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
...init.headers ?? {}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
} catch (error) {
|
|
406
|
+
throw new Error(`Cannot access the broods core service at ${targetUrl}. ` + `Cause: ${error instanceof Error ? error.message : String(error)}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
resolveStatusUrl(status, options) {
|
|
410
|
+
if (typeof status !== "string")
|
|
411
|
+
return status.statusUrl;
|
|
412
|
+
if (/^https?:\/\//.test(status))
|
|
413
|
+
return status;
|
|
414
|
+
if (!options.agentId)
|
|
415
|
+
throw new Error("Polling by status id requires agentId.");
|
|
416
|
+
return statusUrlFor(this.baseUrl, status, options.agentId);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function normalizeHttpServiceUrl(value) {
|
|
420
|
+
const trimmed = value.trim();
|
|
421
|
+
const withProtocol = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
422
|
+
return stripTrailingSlash(withProtocol);
|
|
423
|
+
}
|
|
424
|
+
function directRunBody(input, prefix) {
|
|
425
|
+
const eventId = input.eventId ?? `${prefix}-${Date.now()}`;
|
|
426
|
+
return {
|
|
427
|
+
agentId: input.agentId,
|
|
428
|
+
eventId,
|
|
429
|
+
conversationKey: input.conversationKey ?? eventId,
|
|
430
|
+
events: resolveRunEvents(input),
|
|
431
|
+
...input.system !== undefined ? { system: input.system } : {},
|
|
432
|
+
...input.model !== undefined ? { model: input.model } : {}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function normalizeAsyncAccepted(payload, requestBody) {
|
|
436
|
+
if (!payload || typeof payload !== "object") {
|
|
437
|
+
throw new Error("Async response must be an object");
|
|
438
|
+
}
|
|
439
|
+
const statusUrl = payload.statusUrl;
|
|
440
|
+
if (typeof statusUrl !== "string" || statusUrl.length === 0) {
|
|
441
|
+
throw new Error("Async response missing statusUrl");
|
|
442
|
+
}
|
|
443
|
+
const status = parseStatusUrl(statusUrl);
|
|
444
|
+
if (!status.statusId)
|
|
445
|
+
throw new Error("Async response statusUrl missing status id");
|
|
446
|
+
return {
|
|
447
|
+
statusUrl,
|
|
448
|
+
statusId: status.statusId,
|
|
449
|
+
eventId: status.statusId,
|
|
450
|
+
agentId: status.agentId ?? requestBody.agentId
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function parseStatusUrl(statusUrl) {
|
|
454
|
+
const url = new URL(statusUrl);
|
|
455
|
+
const match = url.pathname.match(/\/status\/([^/]+)$/);
|
|
456
|
+
return {
|
|
457
|
+
statusId: match?.[1] ? decodeURIComponent(match[1]) : undefined,
|
|
458
|
+
agentId: url.searchParams.get("agentId") ?? undefined
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function statusUrlFor(baseUrl, statusId, agentId) {
|
|
462
|
+
return `${normalizeHttpServiceUrl(baseUrl)}/status/${encodeURIComponent(statusId)}?agentId=${encodeURIComponent(agentId)}`;
|
|
463
|
+
}
|
|
464
|
+
async function responseErrorDetails(response, expected) {
|
|
465
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
466
|
+
if (contentType.toLowerCase().includes("text/event-stream")) {
|
|
467
|
+
await response.body?.cancel().catch(() => {});
|
|
468
|
+
return `expected ${expected}, but the server returned an SSE stream. This usually means the core deployment routed /async to the direct streaming runner instead of the async handler.`;
|
|
469
|
+
}
|
|
470
|
+
const text = await response.text();
|
|
471
|
+
if (text.length <= 2000)
|
|
472
|
+
return text;
|
|
473
|
+
return `${text.slice(0, 2000)}... [truncated ${text.length - 2000} chars]`;
|
|
474
|
+
}
|
|
475
|
+
async function cronErrorDetails(response) {
|
|
476
|
+
const text = await response.text();
|
|
477
|
+
if (text.includes("Request body must include eventId and conversationKey")) {
|
|
478
|
+
return `${text}. Cron job APIs must be served by the configured baseUrl. ` + "Prefer defining stable cron jobs with defineCron(...) in broods/ and syncing with `broods dev` or `broods deploy`.";
|
|
479
|
+
}
|
|
480
|
+
return text;
|
|
481
|
+
}
|
|
482
|
+
function resolveCronInput(input) {
|
|
483
|
+
if ("agentId" in input)
|
|
484
|
+
return input;
|
|
485
|
+
const agent = input.agent;
|
|
486
|
+
const agentId = typeof agent === "string" ? agent : agent.id;
|
|
487
|
+
const { agent: _agent, ...rest } = input;
|
|
488
|
+
return { ...rest, agentId };
|
|
489
|
+
}
|
|
490
|
+
function sleep(ms, signal) {
|
|
491
|
+
return new Promise((resolvePromise, reject) => {
|
|
492
|
+
if (signal?.aborted) {
|
|
493
|
+
reject(new Error("Async status polling aborted."));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const timer = setTimeout(resolvePromise, ms);
|
|
497
|
+
signal?.addEventListener("abort", () => {
|
|
498
|
+
clearTimeout(timer);
|
|
499
|
+
reject(new Error("Async status polling aborted."));
|
|
500
|
+
}, { once: true });
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function formatStreamError(error) {
|
|
504
|
+
if (typeof error === "string")
|
|
505
|
+
return error;
|
|
506
|
+
if (!error || typeof error !== "object")
|
|
507
|
+
return String(error);
|
|
508
|
+
const err = error;
|
|
509
|
+
const detail = err.data?.error?.message ?? err.message ?? err.responseBody ?? JSON.stringify(error);
|
|
510
|
+
const prefix = err.name ? `${err.name}: ` : "";
|
|
511
|
+
const status = err.statusCode ? ` (HTTP ${err.statusCode})` : "";
|
|
512
|
+
return `${prefix}${detail}${status}`;
|
|
513
|
+
}
|
|
514
|
+
// src/websocket.ts
|
|
515
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 2000;
|
|
516
|
+
var WS_CONNECTING = 0;
|
|
517
|
+
var WS_OPEN = 1;
|
|
518
|
+
var LAMBDA_FUNCTION_URL_HOST_RE = /\.lambda-url\.[a-z0-9-]+\.on\.aws$/i;
|
|
519
|
+
|
|
520
|
+
class BroodsWebSocketClient {
|
|
521
|
+
baseUrl;
|
|
522
|
+
apiKey;
|
|
523
|
+
WebSocketImpl;
|
|
524
|
+
connectTimeoutMs;
|
|
525
|
+
constructor(options = {}) {
|
|
526
|
+
loadBroodsRuntimeConfig();
|
|
527
|
+
this.baseUrl = normalizeWebSocketServiceUrl(options.baseUrl || process.env.BROODS_WEBSOCKET_URL || options.host || process.env.BROODS_BASE_URL || process.env.BROODS_HOST || DEFAULT_CORE_BASE_URL);
|
|
528
|
+
this.apiKey = options.apiKey || process.env.BROODS_API_KEY || "";
|
|
529
|
+
if (!this.apiKey)
|
|
530
|
+
throw new Error("BroodsWebSocketClient requires apiKey or BROODS_API_KEY.");
|
|
531
|
+
this.WebSocketImpl = options.WebSocket;
|
|
532
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
533
|
+
}
|
|
534
|
+
subscribe(input, handlers = {}) {
|
|
535
|
+
const WebSocketImpl = this.resolveWebSocket();
|
|
536
|
+
const url = this.buildUrl(input);
|
|
537
|
+
const agentId = resolveAgentId(input);
|
|
538
|
+
const accessError = webSocketAccessError(this.baseUrl);
|
|
539
|
+
const socket = new WebSocketImpl(url);
|
|
540
|
+
let opened = false;
|
|
541
|
+
let closed = false;
|
|
542
|
+
const timeout = setTimeout(() => {
|
|
543
|
+
if (!opened)
|
|
544
|
+
fail(accessError);
|
|
545
|
+
}, this.connectTimeoutMs);
|
|
546
|
+
const close = (code = 1000, reason = "client closed") => {
|
|
547
|
+
if (closed)
|
|
548
|
+
return;
|
|
549
|
+
closed = true;
|
|
550
|
+
clearTimeout(timeout);
|
|
551
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
552
|
+
if (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING) {
|
|
553
|
+
socket.close(code, reason);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const fail = (error) => {
|
|
557
|
+
handlers.onError?.(error);
|
|
558
|
+
close(1011, "websocket error");
|
|
559
|
+
};
|
|
560
|
+
const finish = () => {
|
|
561
|
+
handlers.onDone?.();
|
|
562
|
+
close(1000, "done");
|
|
563
|
+
};
|
|
564
|
+
const onAbort = () => {
|
|
565
|
+
if (socket.readyState === WS_OPEN) {
|
|
566
|
+
socket.send(JSON.stringify({ type: "cancel" }));
|
|
567
|
+
}
|
|
568
|
+
fail(new Error("WebSocket run aborted."));
|
|
569
|
+
};
|
|
570
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
571
|
+
socket.onopen = () => {
|
|
572
|
+
opened = true;
|
|
573
|
+
clearTimeout(timeout);
|
|
574
|
+
if (input.signal?.aborted) {
|
|
575
|
+
onAbort();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
socket.send(JSON.stringify({
|
|
579
|
+
type: "execute",
|
|
580
|
+
agentId,
|
|
581
|
+
events: resolveRunEvents(input),
|
|
582
|
+
sessionId: input.sessionId,
|
|
583
|
+
...input.eventId !== undefined ? { eventId: input.eventId } : {},
|
|
584
|
+
...input.system !== undefined ? { system: input.system } : {},
|
|
585
|
+
...input.model !== undefined ? { model: input.model } : {}
|
|
586
|
+
}));
|
|
587
|
+
};
|
|
588
|
+
socket.onmessage = (event) => {
|
|
589
|
+
if (typeof event.data !== "string")
|
|
590
|
+
return;
|
|
591
|
+
const payload = parseServerMessage(event.data);
|
|
592
|
+
if (!payload)
|
|
593
|
+
return;
|
|
594
|
+
handlers.onMessage?.(payload);
|
|
595
|
+
switch (payload.type) {
|
|
596
|
+
case "meta":
|
|
597
|
+
handlers.onMeta?.(payload);
|
|
598
|
+
break;
|
|
599
|
+
case "done":
|
|
600
|
+
finish();
|
|
601
|
+
break;
|
|
602
|
+
case "error":
|
|
603
|
+
fail(new Error(formatWireError(payload.error)));
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
socket.onerror = () => {
|
|
608
|
+
fail(accessError);
|
|
609
|
+
};
|
|
610
|
+
socket.onclose = (event) => {
|
|
611
|
+
clearTimeout(timeout);
|
|
612
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
613
|
+
if (closed || event.code === 1000)
|
|
614
|
+
return;
|
|
615
|
+
fail(new Error(event.reason || "WebSocket connection closed."));
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
url,
|
|
619
|
+
close
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
async* stream(input) {
|
|
623
|
+
const queue = [];
|
|
624
|
+
let done = false;
|
|
625
|
+
let error = null;
|
|
626
|
+
let wake = null;
|
|
627
|
+
const wakeReader = () => {
|
|
628
|
+
wake?.();
|
|
629
|
+
wake = null;
|
|
630
|
+
};
|
|
631
|
+
const subscription = this.subscribe(input, {
|
|
632
|
+
onMessage(message) {
|
|
633
|
+
queue.push(message);
|
|
634
|
+
wakeReader();
|
|
635
|
+
},
|
|
636
|
+
onDone() {
|
|
637
|
+
done = true;
|
|
638
|
+
wakeReader();
|
|
639
|
+
},
|
|
640
|
+
onError(nextError) {
|
|
641
|
+
error = nextError;
|
|
642
|
+
wakeReader();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
while (true) {
|
|
647
|
+
if (queue.length > 0) {
|
|
648
|
+
yield queue.shift();
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (error)
|
|
652
|
+
throw error;
|
|
653
|
+
if (done)
|
|
654
|
+
return;
|
|
655
|
+
await new Promise((resolve2) => {
|
|
656
|
+
wake = resolve2;
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
} finally {
|
|
660
|
+
subscription.close();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
buildUrl(input) {
|
|
664
|
+
const endpointId = resolveEndpointId(input);
|
|
665
|
+
const projectSlug = input.projectSlug ?? input.agent?.projectSlug;
|
|
666
|
+
const environmentSlug = input.environmentSlug ?? input.agent?.environmentSlug;
|
|
667
|
+
const projectPrefix = projectSlug ? `/${projectSlug}` : "";
|
|
668
|
+
const environmentPrefix = environmentSlug ? `/${environmentSlug}` : "";
|
|
669
|
+
const wsBaseUrl = toWebSocketBaseUrl(this.baseUrl);
|
|
670
|
+
return `${wsBaseUrl}/v1${projectPrefix}/agents${environmentPrefix}/${encodeURIComponent(endpointId)}/ws` + `?token=${encodeURIComponent(this.apiKey)}`;
|
|
671
|
+
}
|
|
672
|
+
resolveWebSocket() {
|
|
673
|
+
const WebSocketImpl = this.WebSocketImpl ?? globalThis.WebSocket;
|
|
674
|
+
if (!WebSocketImpl) {
|
|
675
|
+
throw new Error("WebSocket is not available. Pass a WebSocket implementation in BroodsWebSocketClient options.");
|
|
676
|
+
}
|
|
677
|
+
return WebSocketImpl;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function resolveAgentId(input) {
|
|
681
|
+
const agentId = input.agentId ?? input.agent?.id ?? input.endpointId;
|
|
682
|
+
if (!agentId) {
|
|
683
|
+
throw new Error("WebSocket run requires agent, agentId, or endpointId.");
|
|
684
|
+
}
|
|
685
|
+
return agentId;
|
|
686
|
+
}
|
|
687
|
+
function resolveEndpointId(input) {
|
|
688
|
+
const endpointId = input.endpointId ?? input.agent?.endpointId ?? input.agent?.id;
|
|
689
|
+
if (!endpointId) {
|
|
690
|
+
throw new Error("WebSocket run requires agent or endpointId.");
|
|
691
|
+
}
|
|
692
|
+
return endpointId;
|
|
693
|
+
}
|
|
694
|
+
function toWebSocketBaseUrl(url) {
|
|
695
|
+
const parsed = new URL(normalizeWebSocketServiceUrl(url));
|
|
696
|
+
if (parsed.protocol === "https:")
|
|
697
|
+
parsed.protocol = "wss:";
|
|
698
|
+
if (parsed.protocol === "http:")
|
|
699
|
+
parsed.protocol = "ws:";
|
|
700
|
+
return stripTrailingSlash(parsed.toString());
|
|
701
|
+
}
|
|
702
|
+
function normalizeWebSocketServiceUrl(value) {
|
|
703
|
+
const trimmed = value.trim();
|
|
704
|
+
if (/^wss?:\/\//.test(trimmed))
|
|
705
|
+
return stripTrailingSlash(trimmed);
|
|
706
|
+
return normalizeHttpServiceUrl(trimmed);
|
|
707
|
+
}
|
|
708
|
+
function webSocketAccessError(baseUrl) {
|
|
709
|
+
const host = new URL(baseUrl).hostname;
|
|
710
|
+
if (LAMBDA_FUNCTION_URL_HOST_RE.test(host)) {
|
|
711
|
+
return new Error(`Cannot access the WebSocket service at ${baseUrl}. AWS Lambda Function URLs do not support WebSocket upgrades; ` + "set BROODS_WEBSOCKET_URL or BROODS_HOST to the deployed WebSocket gateway URL instead.");
|
|
712
|
+
}
|
|
713
|
+
return new Error(`Cannot access the WebSocket service at ${baseUrl}.`);
|
|
714
|
+
}
|
|
715
|
+
function parseServerMessage(data) {
|
|
716
|
+
try {
|
|
717
|
+
const value = JSON.parse(data);
|
|
718
|
+
return typeof value === "object" && value !== null && typeof value.type === "string" ? value : null;
|
|
719
|
+
} catch {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function formatWireError(error) {
|
|
724
|
+
if (typeof error === "string")
|
|
725
|
+
return error;
|
|
726
|
+
if (error && typeof error === "object" && typeof error.message === "string") {
|
|
727
|
+
return error.message;
|
|
728
|
+
}
|
|
729
|
+
return error === undefined ? "WebSocket stream error." : JSON.stringify(error);
|
|
730
|
+
}
|
|
731
|
+
// src/resources.ts
|
|
732
|
+
var RESOURCE_MARKER = Symbol.for("broods.resource");
|
|
733
|
+
var CONFIG_MARKER = Symbol.for("broods.config");
|
|
734
|
+
var CHANNEL_MARKER = Symbol.for("broods.channel");
|
|
735
|
+
var env = new Proxy(function env2(name) {
|
|
736
|
+
return { __beeblastEnv: true, name };
|
|
737
|
+
}, {
|
|
738
|
+
get(target, property, receiver) {
|
|
739
|
+
if (typeof property === "string")
|
|
740
|
+
return { __beeblastEnv: true, name: property };
|
|
741
|
+
return Reflect.get(target, property, receiver);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
function defineResource(kind, input) {
|
|
745
|
+
if (input.config === undefined) {
|
|
746
|
+
throw new Error(`Resource "${input.name}" must include config`);
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
[RESOURCE_MARKER]: true,
|
|
750
|
+
kind,
|
|
751
|
+
name: input.name,
|
|
752
|
+
...input.description ? { description: input.description } : {},
|
|
753
|
+
config: input.config
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function defineChannel(type, config) {
|
|
757
|
+
return {
|
|
758
|
+
[CHANNEL_MARKER]: true,
|
|
759
|
+
kind: "channel",
|
|
760
|
+
type,
|
|
761
|
+
config
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function defineTelegramChannel(config) {
|
|
765
|
+
return defineChannel("telegram", config);
|
|
766
|
+
}
|
|
767
|
+
function defineGitHubChannel(config) {
|
|
768
|
+
return defineChannel("github", config);
|
|
769
|
+
}
|
|
770
|
+
function defineSlackChannel(config) {
|
|
771
|
+
return defineChannel("slack", config);
|
|
772
|
+
}
|
|
773
|
+
function defineDiscordChannel(config) {
|
|
774
|
+
return defineChannel("discord", config);
|
|
775
|
+
}
|
|
776
|
+
function definePancakeChannel(config) {
|
|
777
|
+
const { ignoreTagIds, ...channelConfig } = config;
|
|
778
|
+
return defineChannel("pancake", {
|
|
779
|
+
...channelConfig,
|
|
780
|
+
...ignoreTagIds === undefined ? {} : { options: { ignoreTagIds } }
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
function defineZaloChannel(config) {
|
|
784
|
+
return defineChannel("zalo", config);
|
|
785
|
+
}
|
|
786
|
+
function defineBroods(config) {
|
|
787
|
+
return { [CONFIG_MARKER]: true, config };
|
|
788
|
+
}
|
|
789
|
+
function defineAgent(input) {
|
|
790
|
+
return defineResource("agent", input);
|
|
791
|
+
}
|
|
792
|
+
function defineWorkspace(input) {
|
|
793
|
+
return defineResource("workspace", input);
|
|
794
|
+
}
|
|
795
|
+
function defineSandbox(input) {
|
|
796
|
+
return defineResource("sandbox", input);
|
|
797
|
+
}
|
|
798
|
+
function defineSkill(input) {
|
|
799
|
+
return defineResource("skill", input);
|
|
800
|
+
}
|
|
801
|
+
function defineTool(input) {
|
|
802
|
+
return defineResource("tool", input);
|
|
803
|
+
}
|
|
804
|
+
function defineCron(input) {
|
|
805
|
+
return defineResource("cron", input);
|
|
806
|
+
}
|
|
807
|
+
function isResource(value) {
|
|
808
|
+
return Boolean(value && typeof value === "object" && value[RESOURCE_MARKER]);
|
|
809
|
+
}
|
|
810
|
+
function isChannelDefinition(value) {
|
|
811
|
+
return Boolean(value && typeof value === "object" && value[CHANNEL_MARKER]);
|
|
812
|
+
}
|
|
813
|
+
function isBroodsConfig(value) {
|
|
814
|
+
return Boolean(value && typeof value === "object" && value[CONFIG_MARKER]);
|
|
815
|
+
}
|
|
816
|
+
export {
|
|
817
|
+
toWebSocketBaseUrl,
|
|
818
|
+
resolveRunEvents,
|
|
819
|
+
readSseStream,
|
|
820
|
+
normalizeHttpServiceUrl,
|
|
821
|
+
isResource,
|
|
822
|
+
isObservabilityClientMessage,
|
|
823
|
+
isChannelDefinition,
|
|
824
|
+
isBroodsConfig,
|
|
825
|
+
env,
|
|
826
|
+
defineZaloChannel,
|
|
827
|
+
defineWorkspace,
|
|
828
|
+
defineTool,
|
|
829
|
+
defineTelegramChannel,
|
|
830
|
+
defineSlackChannel,
|
|
831
|
+
defineSkill,
|
|
832
|
+
defineSandbox,
|
|
833
|
+
definePancakeChannel,
|
|
834
|
+
defineGitHubChannel,
|
|
835
|
+
defineDiscordChannel,
|
|
836
|
+
defineCron,
|
|
837
|
+
defineBroods,
|
|
838
|
+
defineAgent,
|
|
839
|
+
BroodsWebSocketClient as WebsocketClient,
|
|
840
|
+
BroodsWebSocketClient as WebSocketClient,
|
|
841
|
+
MAX_OBSERVABILITY_BACKFILL,
|
|
842
|
+
DEFAULT_CORE_BASE_URL,
|
|
843
|
+
BroodsWebSocketClient,
|
|
844
|
+
BroodsClient
|
|
845
|
+
};
|