@slashfi/agents-sdk 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adk-tools.d.ts +1 -1
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +122 -26
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +5 -9
- package/dist/adk.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts +15 -0
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +42 -17
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/cjs/adk-tools.js +122 -26
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/agent-definitions/remote-registry.js +42 -17
- package/dist/cjs/agent-definitions/remote-registry.js.map +1 -1
- package/dist/cjs/config-store.js +135 -9
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +9 -2
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/events.js +11 -3
- package/dist/cjs/events.js.map +1 -1
- package/dist/cjs/fetch-types.js +3 -0
- package/dist/cjs/fetch-types.js.map +1 -0
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-manager.js +7 -1
- package/dist/cjs/key-manager.js.map +1 -1
- package/dist/cjs/logger.js +115 -0
- package/dist/cjs/logger.js.map +1 -0
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/registry.js +1 -1
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/server.js +70 -13
- package/dist/cjs/server.js.map +1 -1
- package/dist/config-store.d.ts +19 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +135 -9
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +23 -3
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +9 -2
- package/dist/define-config.js.map +1 -1
- package/dist/events.d.ts +6 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +11 -3
- package/dist/events.js.map +1 -1
- package/dist/fetch-types.d.ts +11 -0
- package/dist/fetch-types.d.ts.map +1 -0
- package/dist/fetch-types.js +2 -0
- package/dist/fetch-types.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/key-manager.d.ts +6 -0
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +7 -1
- package/dist/key-manager.js.map +1 -1
- package/dist/logger.d.ts +42 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +109 -0
- package/dist/logger.js.map +1 -0
- package/dist/registry-consumer.d.ts +8 -2
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js.map +1 -1
- package/dist/registry.d.ts +6 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +70 -13
- package/dist/server.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +177 -36
- package/src/adk.ts +5 -10
- package/src/agent-definitions/remote-registry.ts +56 -28
- package/src/config-store.ts +177 -10
- package/src/define-config.ts +25 -4
- package/src/events.ts +16 -6
- package/src/fetch-types.ts +13 -0
- package/src/index.ts +13 -0
- package/src/key-manager.ts +12 -1
- package/src/logger.test.ts +206 -0
- package/src/logger.ts +123 -0
- package/src/ref-naming.test.ts +351 -0
- package/src/registry-consumer.ts +13 -7
- package/src/registry.ts +7 -2
- package/src/server.ts +76 -42
package/src/config-store.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type {
|
|
|
27
27
|
ResolvedRegistry,
|
|
28
28
|
} from "./define-config.js";
|
|
29
29
|
import { normalizeRef } from "./define-config.js";
|
|
30
|
+
import type { FetchFn } from "./fetch-types.js";
|
|
31
|
+
import type { Logger } from "./logger.js";
|
|
30
32
|
import { createRegistryConsumer } from "./registry-consumer.js";
|
|
31
33
|
import type {
|
|
32
34
|
AgentListing,
|
|
@@ -94,6 +96,23 @@ export interface AdkOptions {
|
|
|
94
96
|
* (e.g. client_id/client_secret) before the user auth flow runs.
|
|
95
97
|
*/
|
|
96
98
|
resolveCredentials?: ResolveCredentials;
|
|
99
|
+
/**
|
|
100
|
+
* Custom fetch implementation. Forwarded to every `createRegistryConsumer`
|
|
101
|
+
* call spun up internally by the adk (consumer(), available(), registry.test()).
|
|
102
|
+
*
|
|
103
|
+
* Hosts running inside a long-lived server (e.g. atlas) should pass a
|
|
104
|
+
* hardened fetch — one backed by a connection pool with short timeouts,
|
|
105
|
+
* TCP keepalive, and one-shot retry on connection errors — to avoid
|
|
106
|
+
* dead-socket hangs when upstream pods roll. Defaults to `globalThis.fetch`.
|
|
107
|
+
*/
|
|
108
|
+
fetch?: FetchFn;
|
|
109
|
+
/**
|
|
110
|
+
* Structured logger. Currently reserved for future use; the adk itself
|
|
111
|
+
* does not emit logs today but may in future versions. Threading this in
|
|
112
|
+
* now lets hosts standardize on a single logger across all sdk surfaces
|
|
113
|
+
* (`createAgentRegistry`, `createAgentServer`, `createAdk`, etc.).
|
|
114
|
+
*/
|
|
115
|
+
logger?: Logger;
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
export interface RegistryTestResult {
|
|
@@ -353,6 +372,74 @@ function isUnauthorized(result: unknown): boolean {
|
|
|
353
372
|
return false;
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
// ============================================
|
|
376
|
+
// Local auth form HTML
|
|
377
|
+
// ============================================
|
|
378
|
+
|
|
379
|
+
const esc = (s: string) =>
|
|
380
|
+
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
381
|
+
|
|
382
|
+
function renderCredentialForm(name: string, fields: AuthChallengeField[], error?: string): string {
|
|
383
|
+
const fieldHtml = fields.map((f) => `
|
|
384
|
+
<div class="field">
|
|
385
|
+
<label for="${esc(f.name)}">${esc(f.label)}</label>
|
|
386
|
+
${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
|
|
387
|
+
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
|
|
388
|
+
</div>`).join("");
|
|
389
|
+
|
|
390
|
+
const errorHtml = error
|
|
391
|
+
? `<div class="error">${esc(error)}</div>`
|
|
392
|
+
: "";
|
|
393
|
+
|
|
394
|
+
return `<!DOCTYPE html>
|
|
395
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
396
|
+
<title>Authenticate \u2014 ${esc(name)}</title>
|
|
397
|
+
<style>
|
|
398
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
399
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0a;color:#e5e5e5;display:flex;justify-content:center;align-items:center;min-height:100vh}
|
|
400
|
+
.card{background:#141414;border:1px solid #262626;border-radius:12px;padding:32px;width:100%;max-width:420px}
|
|
401
|
+
h1{font-size:20px;font-weight:600;color:#fafafa;margin-bottom:4px}
|
|
402
|
+
.sub{font-size:14px;color:#a3a3a3;margin-bottom:24px}
|
|
403
|
+
.field{margin-bottom:16px}
|
|
404
|
+
label{display:block;font-size:13px;font-weight:500;color:#d4d4d4;margin-bottom:6px}
|
|
405
|
+
.desc{font-size:12px;color:#737373;margin-bottom:6px}
|
|
406
|
+
input{width:100%;padding:10px 12px;background:#0a0a0a;border:1px solid #333;border-radius:8px;color:#fafafa;font-size:14px;font-family:'SF Mono',Menlo,Consolas,monospace;outline:none;transition:border-color .15s}
|
|
407
|
+
input:focus{border-color:#3b82f6}
|
|
408
|
+
button{width:100%;padding:10px;background:#f5f5f5;color:#0a0a0a;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;margin-top:8px;transition:background .15s}
|
|
409
|
+
button:hover{background:#e5e5e5}
|
|
410
|
+
.error{font-size:13px;color:#f87171;margin-bottom:16px;padding:10px 12px;background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.2);border-radius:8px}
|
|
411
|
+
</style></head><body>
|
|
412
|
+
<div class="card">
|
|
413
|
+
<h1>Authenticate</h1>
|
|
414
|
+
<p class="sub">${esc(name)}</p>
|
|
415
|
+
${errorHtml}
|
|
416
|
+
<form method="POST">${fieldHtml}
|
|
417
|
+
<button type="submit">Authenticate</button>
|
|
418
|
+
</form>
|
|
419
|
+
</div>
|
|
420
|
+
</body></html>`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function renderAuthSuccess(name: string): string {
|
|
424
|
+
return `<!DOCTYPE html>
|
|
425
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
426
|
+
<title>Authenticated \u2014 ${esc(name)}</title>
|
|
427
|
+
<style>
|
|
428
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
429
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0a;color:#e5e5e5;display:flex;justify-content:center;align-items:center;min-height:100vh}
|
|
430
|
+
.card{background:#141414;border:1px solid #262626;border-radius:12px;padding:32px 48px;width:100%;max-width:420px;text-align:center}
|
|
431
|
+
.icon{font-size:48px;margin-bottom:16px;color:#22c55e}
|
|
432
|
+
h1{font-size:20px;font-weight:600;color:#fafafa;margin-bottom:4px}
|
|
433
|
+
p{font-size:14px;color:#a3a3a3}
|
|
434
|
+
</style></head><body>
|
|
435
|
+
<div class="card">
|
|
436
|
+
<div class="icon">✓</div>
|
|
437
|
+
<h1>Authenticated</h1>
|
|
438
|
+
<p>${esc(name)} is ready to use. You can close this tab.</p>
|
|
439
|
+
</div>
|
|
440
|
+
</body></html>`;
|
|
441
|
+
}
|
|
442
|
+
|
|
356
443
|
export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
357
444
|
|
|
358
445
|
async function readConfig(): Promise<ConsumerConfig> {
|
|
@@ -579,7 +666,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
579
666
|
|
|
580
667
|
return createRegistryConsumer(
|
|
581
668
|
{ registries: resolved, refs: config.refs ?? [] },
|
|
582
|
-
{ token: options.token },
|
|
669
|
+
{ token: options.token, fetch: options.fetch },
|
|
583
670
|
);
|
|
584
671
|
}
|
|
585
672
|
|
|
@@ -617,7 +704,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
617
704
|
|
|
618
705
|
return createRegistryConsumer(
|
|
619
706
|
{ registries: resolved, refs: config.refs ?? [] },
|
|
620
|
-
{ token: options.token },
|
|
707
|
+
{ token: options.token, fetch: options.fetch },
|
|
621
708
|
);
|
|
622
709
|
}
|
|
623
710
|
|
|
@@ -725,7 +812,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
725
812
|
const url = registryUrl(r);
|
|
726
813
|
const rName = registryDisplayName(r);
|
|
727
814
|
try {
|
|
728
|
-
const consumer = await createRegistryConsumer(
|
|
815
|
+
const consumer = await createRegistryConsumer(
|
|
816
|
+
{ registries: [r] },
|
|
817
|
+
{ token: options.token, fetch: options.fetch },
|
|
818
|
+
);
|
|
729
819
|
const disc = await consumer.discover(url);
|
|
730
820
|
return { name: rName, url, status: "active", issuer: disc.issuer };
|
|
731
821
|
} catch (err: unknown) {
|
|
@@ -886,7 +976,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
886
976
|
found = true;
|
|
887
977
|
const updated = { ...r };
|
|
888
978
|
if (updates.url) updated.url = updates.url;
|
|
889
|
-
|
|
979
|
+
// Rename: prefer `name`, fall back to legacy `as`. When the
|
|
980
|
+
// caller passes `name`, clear the legacy `as` so the stored
|
|
981
|
+
// entry has one source of truth.
|
|
982
|
+
if (updates.name !== undefined) {
|
|
983
|
+
updated.name = updates.name;
|
|
984
|
+
if (updated.as !== undefined) updated.as = undefined;
|
|
985
|
+
} else if (updates.as !== undefined) {
|
|
986
|
+
updated.as = updates.as;
|
|
987
|
+
}
|
|
890
988
|
if (updates.scheme) updated.scheme = updates.scheme;
|
|
891
989
|
if (updates.config) updated.config = { ...updated.config, ...updates.config };
|
|
892
990
|
if (updates.sourceRegistry) updated.sourceRegistry = updates.sourceRegistry;
|
|
@@ -1392,19 +1490,88 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1392
1490
|
const result = await ref.auth(name);
|
|
1393
1491
|
|
|
1394
1492
|
if (result.complete) return { complete: true };
|
|
1493
|
+
|
|
1494
|
+
const port = options.oauthCallbackPort ?? 8919;
|
|
1495
|
+
const timeout = opts?.timeoutMs ?? 300_000;
|
|
1496
|
+
const { createServer } = await import("node:http");
|
|
1497
|
+
|
|
1498
|
+
// API key / HTTP auth — serve a local credential form
|
|
1499
|
+
if (result.fields && result.fields.length > 0 && result.type !== "oauth2") {
|
|
1500
|
+
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1501
|
+
const server = createServer(async (req, res) => {
|
|
1502
|
+
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
1503
|
+
|
|
1504
|
+
if (req.method === "GET" && reqUrl.pathname === "/auth") {
|
|
1505
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1506
|
+
res.end(renderCredentialForm(name, result.fields!));
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (req.method === "POST" && reqUrl.pathname === "/auth") {
|
|
1511
|
+
const chunks: Buffer[] = [];
|
|
1512
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
1513
|
+
const body = Buffer.concat(chunks).toString();
|
|
1514
|
+
const params = new URLSearchParams(body);
|
|
1515
|
+
|
|
1516
|
+
const credentials: Record<string, string> = {};
|
|
1517
|
+
for (const field of result.fields!) {
|
|
1518
|
+
const val = params.get(field.name);
|
|
1519
|
+
if (val) credentials[field.name] = val;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
try {
|
|
1523
|
+
const authResult = await ref.auth(name, { credentials });
|
|
1524
|
+
if (authResult.complete) {
|
|
1525
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1526
|
+
res.end(renderAuthSuccess(name));
|
|
1527
|
+
server.close();
|
|
1528
|
+
resolve({ complete: true });
|
|
1529
|
+
} else {
|
|
1530
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1531
|
+
res.end(renderCredentialForm(
|
|
1532
|
+
name,
|
|
1533
|
+
authResult.fields ?? result.fields!,
|
|
1534
|
+
"Some credentials were missing or invalid.",
|
|
1535
|
+
));
|
|
1536
|
+
}
|
|
1537
|
+
} catch (err) {
|
|
1538
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
1539
|
+
res.end(renderCredentialForm(
|
|
1540
|
+
name,
|
|
1541
|
+
result.fields!,
|
|
1542
|
+
err instanceof Error ? err.message : String(err),
|
|
1543
|
+
));
|
|
1544
|
+
}
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
res.writeHead(404);
|
|
1549
|
+
res.end();
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
server.listen(port, () => {
|
|
1553
|
+
if (opts?.onAuthorizeUrl) {
|
|
1554
|
+
opts.onAuthorizeUrl(`http://localhost:${port}/auth`);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
const timer = setTimeout(() => {
|
|
1559
|
+
server.close();
|
|
1560
|
+
reject(new Error("Auth timed out"));
|
|
1561
|
+
}, timeout);
|
|
1562
|
+
server.on("close", () => clearTimeout(timer));
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// OAuth2 — open authorize URL and wait for callback
|
|
1395
1567
|
if (result.type !== "oauth2" || !result.authorizeUrl) {
|
|
1396
|
-
throw new Error(`authLocal
|
|
1568
|
+
throw new Error(`authLocal cannot handle auth type: ${result.type}`);
|
|
1397
1569
|
}
|
|
1398
1570
|
|
|
1399
1571
|
if (opts?.onAuthorizeUrl) {
|
|
1400
1572
|
opts.onAuthorizeUrl(result.authorizeUrl);
|
|
1401
1573
|
}
|
|
1402
1574
|
|
|
1403
|
-
// Spin up local callback server
|
|
1404
|
-
const port = options.oauthCallbackPort ?? 8919;
|
|
1405
|
-
const timeout = opts?.timeoutMs ?? 300_000;
|
|
1406
|
-
|
|
1407
|
-
const { createServer } = await import("node:http");
|
|
1408
1575
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1409
1576
|
const server = createServer(async (req, res) => {
|
|
1410
1577
|
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
package/src/define-config.ts
CHANGED
|
@@ -64,16 +64,30 @@ export type RefConfig = Record<string, unknown>;
|
|
|
64
64
|
|
|
65
65
|
/** A ref entry — describes how to connect to an agent */
|
|
66
66
|
export type RefEntry = {
|
|
67
|
-
/**
|
|
67
|
+
/** Canonical agent path on the remote registry (e.g. `notion`, `linear`). */
|
|
68
68
|
ref: string;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Local identifier for this ref. Used by all operations
|
|
72
|
+
* (call/remove/auth/update/…) to look up the entry. If omitted,
|
|
73
|
+
* the canonical `ref` string is used as the identifier — the
|
|
74
|
+
* common case "one local instance per agent" requires only
|
|
75
|
+
* `{ ref: 'notion', ... }`. Set `name` to a different value only
|
|
76
|
+
* when you need multiple local instances of the same remote
|
|
77
|
+
* agent (e.g. `{ ref: 'notion', name: 'work-notion' }`).
|
|
78
|
+
*/
|
|
79
|
+
name?: string;
|
|
80
|
+
|
|
70
81
|
/** Connection scheme */
|
|
71
82
|
scheme?: 'mcp' | 'https' | 'registry';
|
|
72
83
|
|
|
73
84
|
/** Direct URL to the agent (e.g. https://mcp.notion.com/mcp) */
|
|
74
85
|
url?: string;
|
|
75
86
|
|
|
76
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Use `name` instead. `as` is preserved for reading
|
|
89
|
+
* old consumer-config.json files; new writes emit `name`.
|
|
90
|
+
*/
|
|
77
91
|
as?: string;
|
|
78
92
|
|
|
79
93
|
/** Per-instance config (headers, secrets, etc. — values support {{secret-uri}} templates) */
|
|
@@ -176,11 +190,18 @@ export interface ResolvedConfig {
|
|
|
176
190
|
// Helpers
|
|
177
191
|
// ============================================
|
|
178
192
|
|
|
179
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Normalize a ref entry to its full form.
|
|
195
|
+
*
|
|
196
|
+
* Local identifier resolution order: `entry.name` → `entry.as` (legacy)
|
|
197
|
+
* → `entry.ref` (canonical). This order makes the tool/API surface
|
|
198
|
+
* consistent with the `ref.add({ ref, name })` contract while still
|
|
199
|
+
* reading old `{ ref, as }` entries from pre-0.74 consumer-config.json.
|
|
200
|
+
*/
|
|
180
201
|
export function normalizeRef(entry: RefEntry): ResolvedRef {
|
|
181
202
|
return {
|
|
182
203
|
...entry,
|
|
183
|
-
name: entry.as ?? entry.ref,
|
|
204
|
+
name: entry.name ?? entry.as ?? entry.ref,
|
|
184
205
|
config: entry.config ?? {},
|
|
185
206
|
};
|
|
186
207
|
}
|
package/src/events.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Filtering happens in the callback, not the API.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { getDefaultLogger, type Logger } from "./logger.js";
|
|
13
14
|
import type { AgentDefinition, CallAgentRequest, CallAgentResponse } from "./types.js";
|
|
14
15
|
// =============================================================================
|
|
15
16
|
// Event Types
|
|
@@ -293,11 +294,17 @@ export interface EventBus {
|
|
|
293
294
|
): void;
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
export interface EventBusOptions {
|
|
298
|
+
/** Logger for listener errors (default: module-level default logger) */
|
|
299
|
+
logger?: Logger;
|
|
300
|
+
}
|
|
301
|
+
|
|
296
302
|
/**
|
|
297
303
|
* Create an event bus.
|
|
298
304
|
*/
|
|
299
|
-
export function createEventBus(): EventBus {
|
|
305
|
+
export function createEventBus(options: EventBusOptions = {}): EventBus {
|
|
300
306
|
const listeners: ListenerEntry[] = [];
|
|
307
|
+
const logger = options.logger ?? getDefaultLogger();
|
|
301
308
|
|
|
302
309
|
function on<T extends EventType>(
|
|
303
310
|
eventType: T,
|
|
@@ -346,11 +353,14 @@ export function createEventBus(): EventBus {
|
|
|
346
353
|
try {
|
|
347
354
|
await listener.callback(event as never);
|
|
348
355
|
} catch (err) {
|
|
349
|
-
// Never propagate listener errors — log and continue
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
356
|
+
// Never propagate listener errors — log and continue.
|
|
357
|
+
// Structured fields so Datadog keeps the whole record as one event.
|
|
358
|
+
logger.error("event_listener_error", {
|
|
359
|
+
component: "agents-sdk.events",
|
|
360
|
+
event_type: event.type,
|
|
361
|
+
agent_path: event.agentPath,
|
|
362
|
+
error: err,
|
|
363
|
+
});
|
|
354
364
|
}
|
|
355
365
|
}
|
|
356
366
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural `fetch` type that's compatible across Node (undici-backed) and
|
|
3
|
+
* Bun (which extends the global fetch with extras like `preconnect`).
|
|
4
|
+
*
|
|
5
|
+
* Using `typeof globalThis.fetch` in options types causes type-errors when a
|
|
6
|
+
* Node-typed fetch implementation (e.g. an undici.Agent-backed wrapper) is
|
|
7
|
+
* passed in a codebase whose @types/bun is loaded. This structural subset is
|
|
8
|
+
* the minimum surface the SDK actually uses and both runtimes satisfy it.
|
|
9
|
+
*/
|
|
10
|
+
export type FetchFn = (
|
|
11
|
+
input: string | URL | Request,
|
|
12
|
+
init?: RequestInit,
|
|
13
|
+
) => Promise<Response>;
|
package/src/index.ts
CHANGED
|
@@ -126,6 +126,7 @@ export type {
|
|
|
126
126
|
export { createEventBus } from "./events.js";
|
|
127
127
|
export type {
|
|
128
128
|
EventBus,
|
|
129
|
+
EventBusOptions,
|
|
129
130
|
EventType,
|
|
130
131
|
SystemEventType,
|
|
131
132
|
CustomEventMap,
|
|
@@ -145,6 +146,18 @@ export type {
|
|
|
145
146
|
ListenerEntry,
|
|
146
147
|
} from "./events.js";
|
|
147
148
|
|
|
149
|
+
// Logger
|
|
150
|
+
export {
|
|
151
|
+
createConsoleJsonLogger,
|
|
152
|
+
createNoopLogger,
|
|
153
|
+
getDefaultLogger,
|
|
154
|
+
setDefaultLogger,
|
|
155
|
+
} from "./logger.js";
|
|
156
|
+
export type { LogFields, LogLevel, Logger } from "./logger.js";
|
|
157
|
+
|
|
158
|
+
// Structural fetch type
|
|
159
|
+
export type { FetchFn } from "./fetch-types.js";
|
|
160
|
+
|
|
148
161
|
// Server
|
|
149
162
|
export {
|
|
150
163
|
createAgentServer,
|
package/src/key-manager.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
generateKeyPair,
|
|
22
22
|
importJWK,
|
|
23
23
|
} from "jose";
|
|
24
|
+
import { getDefaultLogger, type Logger } from "./logger.js";
|
|
24
25
|
|
|
25
26
|
// ── Types ──
|
|
26
27
|
|
|
@@ -88,6 +89,11 @@ export interface KeyManagerOptions {
|
|
|
88
89
|
tokenTtlSeconds?: number;
|
|
89
90
|
/** Enable background key rotation (default: true). Set to false on read-only replicas. */
|
|
90
91
|
enableRotation?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Structured logger for rotation errors. Defaults to the module-level
|
|
94
|
+
* default logger (single-line JSON).
|
|
95
|
+
*/
|
|
96
|
+
logger?: Logger;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
// ── Constants ──
|
|
@@ -148,6 +154,7 @@ export async function createKeyManager(
|
|
|
148
154
|
tokenTtlSeconds = 300,
|
|
149
155
|
enableRotation = true,
|
|
150
156
|
} = opts;
|
|
157
|
+
const logger = opts.logger ?? getDefaultLogger();
|
|
151
158
|
|
|
152
159
|
let keys: CachedKey[] = [];
|
|
153
160
|
|
|
@@ -221,7 +228,11 @@ export async function createKeyManager(
|
|
|
221
228
|
try {
|
|
222
229
|
await checkAndRotate();
|
|
223
230
|
} catch (err) {
|
|
224
|
-
|
|
231
|
+
logger.error("key_rotation_failed", {
|
|
232
|
+
component: "agents-sdk.key-manager",
|
|
233
|
+
issuer,
|
|
234
|
+
error: err,
|
|
235
|
+
});
|
|
225
236
|
}
|
|
226
237
|
}, checkIntervalMs)
|
|
227
238
|
: null;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createEventBus } from "./events.js";
|
|
3
|
+
import {
|
|
4
|
+
type Logger,
|
|
5
|
+
createConsoleJsonLogger,
|
|
6
|
+
createNoopLogger,
|
|
7
|
+
getDefaultLogger,
|
|
8
|
+
setDefaultLogger,
|
|
9
|
+
} from "./logger.js";
|
|
10
|
+
|
|
11
|
+
function captureLogger(): { logger: Logger; records: unknown[] } {
|
|
12
|
+
const records: unknown[] = [];
|
|
13
|
+
const logger: Logger = {
|
|
14
|
+
debug: (msg, fields) => records.push({ level: "debug", msg, fields }),
|
|
15
|
+
info: (msg, fields) => records.push({ level: "info", msg, fields }),
|
|
16
|
+
warn: (msg, fields) => records.push({ level: "warn", msg, fields }),
|
|
17
|
+
error: (msg, fields) => records.push({ level: "error", msg, fields }),
|
|
18
|
+
with: () => logger,
|
|
19
|
+
};
|
|
20
|
+
return { logger, records };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("createConsoleJsonLogger", () => {
|
|
24
|
+
test("emits single-line JSON for each record", () => {
|
|
25
|
+
const lines: string[] = [];
|
|
26
|
+
const originalError = console.error;
|
|
27
|
+
const originalLog = console.log;
|
|
28
|
+
console.error = (line: string) => lines.push(line);
|
|
29
|
+
console.log = (line: string) => lines.push(line);
|
|
30
|
+
try {
|
|
31
|
+
const logger = createConsoleJsonLogger();
|
|
32
|
+
logger.info("hello", { foo: "bar" });
|
|
33
|
+
logger.error("oops", { err: new Error("boom") });
|
|
34
|
+
} finally {
|
|
35
|
+
console.error = originalError;
|
|
36
|
+
console.log = originalLog;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect(lines.length).toBe(2);
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
// Single line
|
|
42
|
+
expect(line.includes("\n")).toBe(false);
|
|
43
|
+
// Valid JSON
|
|
44
|
+
expect(() => JSON.parse(line)).not.toThrow();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const info = JSON.parse(lines[0]!);
|
|
48
|
+
expect(info.level).toBe("info");
|
|
49
|
+
expect(info.message).toBe("hello");
|
|
50
|
+
expect(info.foo).toBe("bar");
|
|
51
|
+
expect(typeof info.timestamp).toBe("string");
|
|
52
|
+
|
|
53
|
+
const err = JSON.parse(lines[1]!);
|
|
54
|
+
expect(err.level).toBe("error");
|
|
55
|
+
expect(err.message).toBe("oops");
|
|
56
|
+
expect(err.err.name).toBe("Error");
|
|
57
|
+
expect(err.err.message).toBe("boom");
|
|
58
|
+
expect(typeof err.err.stack).toBe("string");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("with() merges base fields into child records", () => {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
const originalLog = console.log;
|
|
64
|
+
console.log = (line: string) => lines.push(line);
|
|
65
|
+
try {
|
|
66
|
+
const root = createConsoleJsonLogger({ service: "atlas-api" });
|
|
67
|
+
const child = root.with({ request_id: "abc123" });
|
|
68
|
+
child.info("req", { method: "GET" });
|
|
69
|
+
} finally {
|
|
70
|
+
console.log = originalLog;
|
|
71
|
+
}
|
|
72
|
+
expect(lines.length).toBe(1);
|
|
73
|
+
const record = JSON.parse(lines[0]!);
|
|
74
|
+
expect(record.service).toBe("atlas-api");
|
|
75
|
+
expect(record.request_id).toBe("abc123");
|
|
76
|
+
expect(record.method).toBe("GET");
|
|
77
|
+
expect(record.message).toBe("req");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("handles non-serializable values without throwing", () => {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
const originalLog = console.log;
|
|
83
|
+
console.log = (line: string) => lines.push(line);
|
|
84
|
+
try {
|
|
85
|
+
const logger = createConsoleJsonLogger();
|
|
86
|
+
const circular: Record<string, unknown> = {};
|
|
87
|
+
circular.self = circular;
|
|
88
|
+
logger.info("cycle", { circular });
|
|
89
|
+
} finally {
|
|
90
|
+
console.log = originalLog;
|
|
91
|
+
}
|
|
92
|
+
expect(lines.length).toBe(1);
|
|
93
|
+
const record = JSON.parse(lines[0]!);
|
|
94
|
+
expect(record.serializer_error).toBe(true);
|
|
95
|
+
expect(record.message).toBe("cycle");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("serializes bigint and drops functions", () => {
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
const originalLog = console.log;
|
|
101
|
+
console.log = (line: string) => lines.push(line);
|
|
102
|
+
try {
|
|
103
|
+
const logger = createConsoleJsonLogger();
|
|
104
|
+
logger.info("values", { big: 42n, fn: () => "ignored" });
|
|
105
|
+
} finally {
|
|
106
|
+
console.log = originalLog;
|
|
107
|
+
}
|
|
108
|
+
const record = JSON.parse(lines[0]!);
|
|
109
|
+
expect(record.big).toBe("42");
|
|
110
|
+
expect(record.fn).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("createNoopLogger", () => {
|
|
115
|
+
test("drops every record and with() returns self", () => {
|
|
116
|
+
const noop = createNoopLogger();
|
|
117
|
+
expect(() => {
|
|
118
|
+
noop.debug("d");
|
|
119
|
+
noop.info("i");
|
|
120
|
+
noop.warn("w");
|
|
121
|
+
noop.error("e", { err: new Error("ignored") });
|
|
122
|
+
}).not.toThrow();
|
|
123
|
+
const child = noop.with({ ctx: "x" });
|
|
124
|
+
expect(typeof child.info).toBe("function");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("default logger", () => {
|
|
129
|
+
test("setDefaultLogger / getDefaultLogger round-trip", () => {
|
|
130
|
+
const original = getDefaultLogger();
|
|
131
|
+
const { logger, records } = captureLogger();
|
|
132
|
+
try {
|
|
133
|
+
setDefaultLogger(logger);
|
|
134
|
+
getDefaultLogger().info("routed");
|
|
135
|
+
expect(records.length).toBe(1);
|
|
136
|
+
} finally {
|
|
137
|
+
setDefaultLogger(original);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("createEventBus uses injected logger", () => {
|
|
143
|
+
test("listener error is logged as a single structured record", async () => {
|
|
144
|
+
const { logger, records } = captureLogger();
|
|
145
|
+
const bus = createEventBus({ logger });
|
|
146
|
+
bus.on("tool/call", () => {
|
|
147
|
+
throw new Error("listener boom");
|
|
148
|
+
});
|
|
149
|
+
await bus.emit({
|
|
150
|
+
type: "tool/call",
|
|
151
|
+
agentPath: "@test",
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
tool: "demo",
|
|
154
|
+
params: {},
|
|
155
|
+
});
|
|
156
|
+
expect(records.length).toBe(1);
|
|
157
|
+
const rec = records[0] as {
|
|
158
|
+
level: string;
|
|
159
|
+
msg: string;
|
|
160
|
+
fields: Record<string, unknown>;
|
|
161
|
+
};
|
|
162
|
+
expect(rec.level).toBe("error");
|
|
163
|
+
expect(rec.msg).toBe("event_listener_error");
|
|
164
|
+
expect(rec.fields.component).toBe("agents-sdk.events");
|
|
165
|
+
expect(rec.fields.event_type).toBe("tool/call");
|
|
166
|
+
expect(rec.fields.agent_path).toBe("@test");
|
|
167
|
+
expect(rec.fields.error).toBeInstanceOf(Error);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("listener errors never propagate", async () => {
|
|
171
|
+
const bus = createEventBus({ logger: createNoopLogger() });
|
|
172
|
+
bus.on("invoke", () => {
|
|
173
|
+
throw new Error("nope");
|
|
174
|
+
});
|
|
175
|
+
await expect(
|
|
176
|
+
bus.emit({
|
|
177
|
+
type: "invoke",
|
|
178
|
+
agentPath: "@test",
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
prompt: "hi",
|
|
181
|
+
}),
|
|
182
|
+
).resolves.toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("falls back to module default logger when no option passed", async () => {
|
|
186
|
+
const original = getDefaultLogger();
|
|
187
|
+
const { logger, records } = captureLogger();
|
|
188
|
+
try {
|
|
189
|
+
setDefaultLogger(logger);
|
|
190
|
+
const bus = createEventBus();
|
|
191
|
+
bus.on("tool/call", () => {
|
|
192
|
+
throw new Error("captured");
|
|
193
|
+
});
|
|
194
|
+
await bus.emit({
|
|
195
|
+
type: "tool/call",
|
|
196
|
+
agentPath: "@test",
|
|
197
|
+
timestamp: Date.now(),
|
|
198
|
+
tool: "demo",
|
|
199
|
+
params: {},
|
|
200
|
+
});
|
|
201
|
+
expect(records.length).toBe(1);
|
|
202
|
+
} finally {
|
|
203
|
+
setDefaultLogger(original);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|