@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/logger.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger — pluggable structured logger for the agents-sdk.
|
|
3
|
+
*
|
|
4
|
+
* The SDK emits errors and traces via this interface so consumers (e.g. atlas)
|
|
5
|
+
* can route logs into their own observability stack. The default logger writes
|
|
6
|
+
* single-line JSON to stdout/stderr, which keeps Datadog from splitting
|
|
7
|
+
* multi-line stack traces into separate events.
|
|
8
|
+
*
|
|
9
|
+
* @example Inject your own logger
|
|
10
|
+
* ```ts
|
|
11
|
+
* const registry = createAgentRegistry({ logger: myStructuredLogger });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @example Disable all SDK logging
|
|
15
|
+
* ```ts
|
|
16
|
+
* const registry = createAgentRegistry({ logger: createNoopLogger() });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type LogFields = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
23
|
+
|
|
24
|
+
export interface Logger {
|
|
25
|
+
debug(message: string, fields?: LogFields): void;
|
|
26
|
+
info(message: string, fields?: LogFields): void;
|
|
27
|
+
warn(message: string, fields?: LogFields): void;
|
|
28
|
+
error(message: string, fields?: LogFields): void;
|
|
29
|
+
/** Return a child logger that merges the given fields into every record. */
|
|
30
|
+
with(fields: LogFields): Logger;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Replacer that expands Error objects into plain JSON-serializable fields
|
|
35
|
+
* (name/message/stack/cause), and falls back to String() for other
|
|
36
|
+
* non-serializable values so the logger never throws or drops the message.
|
|
37
|
+
*/
|
|
38
|
+
function errorReplacer(_key: string, value: unknown): unknown {
|
|
39
|
+
if (value instanceof Error) {
|
|
40
|
+
return {
|
|
41
|
+
name: value.name,
|
|
42
|
+
message: value.message,
|
|
43
|
+
stack: value.stack,
|
|
44
|
+
...(value.cause !== undefined ? { cause: value.cause } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "bigint") return value.toString();
|
|
48
|
+
if (typeof value === "function") return undefined;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default logger: emits a single JSON line per record.
|
|
54
|
+
* - debug/info → stdout
|
|
55
|
+
* - warn/error → stderr
|
|
56
|
+
*
|
|
57
|
+
* Kept intentionally minimal. Hosts should replace this with their own
|
|
58
|
+
* transport in production.
|
|
59
|
+
*/
|
|
60
|
+
export function createConsoleJsonLogger(base: LogFields = {}): Logger {
|
|
61
|
+
function emit(level: LogLevel, message: string, fields?: LogFields): void {
|
|
62
|
+
const record = {
|
|
63
|
+
level,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
message,
|
|
66
|
+
...base,
|
|
67
|
+
...fields,
|
|
68
|
+
};
|
|
69
|
+
let line: string;
|
|
70
|
+
try {
|
|
71
|
+
line = JSON.stringify(record, errorReplacer);
|
|
72
|
+
} catch {
|
|
73
|
+
// Last-ditch fallback: if the payload can't be serialized, at least
|
|
74
|
+
// emit the message so callers aren't flying blind.
|
|
75
|
+
line = JSON.stringify({
|
|
76
|
+
level,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
message,
|
|
79
|
+
serializer_error: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (level === "error" || level === "warn") {
|
|
83
|
+
console.error(line);
|
|
84
|
+
} else {
|
|
85
|
+
console.log(line);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
debug: (message, fields) => emit("debug", message, fields),
|
|
90
|
+
info: (message, fields) => emit("info", message, fields),
|
|
91
|
+
warn: (message, fields) => emit("warn", message, fields),
|
|
92
|
+
error: (message, fields) => emit("error", message, fields),
|
|
93
|
+
with: (fields) => createConsoleJsonLogger({ ...base, ...fields }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Logger that drops every record. Useful for silencing SDK output in tests. */
|
|
98
|
+
export function createNoopLogger(): Logger {
|
|
99
|
+
const noop = (): void => {};
|
|
100
|
+
const self: Logger = {
|
|
101
|
+
debug: noop,
|
|
102
|
+
info: noop,
|
|
103
|
+
warn: noop,
|
|
104
|
+
error: noop,
|
|
105
|
+
with: () => self,
|
|
106
|
+
};
|
|
107
|
+
return self;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Module-level default logger. Hosts that want JSON output everywhere can
|
|
112
|
+
* set this once at startup instead of threading a logger through every
|
|
113
|
+
* factory call.
|
|
114
|
+
*/
|
|
115
|
+
let defaultLogger: Logger = createConsoleJsonLogger();
|
|
116
|
+
|
|
117
|
+
export function setDefaultLogger(logger: Logger): void {
|
|
118
|
+
defaultLogger = logger;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getDefaultLogger(): Logger {
|
|
122
|
+
return defaultLogger;
|
|
123
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `ref` naming contract introduced in 0.74:
|
|
3
|
+
*
|
|
4
|
+
* - `RefEntry` gains an optional `name?` field for the local identifier.
|
|
5
|
+
* The legacy `as?` field still parses for backward compat.
|
|
6
|
+
* - `normalizeRef` resolves the identifier as `name ?? as ?? ref`, so
|
|
7
|
+
* all lookup paths (get, list, update, remove) accept entries written
|
|
8
|
+
* in either shape.
|
|
9
|
+
* - `createRefTool` (the adk-tools.ts MCP tool) drops `as` from its
|
|
10
|
+
* schema and defaults `ref` to `name` on add, so "Add a ref called X"
|
|
11
|
+
* via an LLM that picks either field lands on the same stored
|
|
12
|
+
* `{ ref: 'X' }` entry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from "bun:test";
|
|
16
|
+
import { createAdkTools } from "./adk-tools";
|
|
17
|
+
import type { FsStore } from "./agent-definitions/config";
|
|
18
|
+
import { createAdk } from "./index";
|
|
19
|
+
import type { ToolContext } from "./types";
|
|
20
|
+
|
|
21
|
+
function createMemoryFs(): FsStore {
|
|
22
|
+
const files = new Map<string, string>();
|
|
23
|
+
return {
|
|
24
|
+
async readFile(path: string) {
|
|
25
|
+
return files.get(path) ?? null;
|
|
26
|
+
},
|
|
27
|
+
async writeFile(path: string, content: string) {
|
|
28
|
+
files.set(path, content);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read a known-present file and parse it as JSON, with typed assertion. */
|
|
34
|
+
async function readJson<T = unknown>(fs: FsStore, path: string): Promise<T> {
|
|
35
|
+
const raw = await fs.readFile(path);
|
|
36
|
+
if (raw === null) {
|
|
37
|
+
throw new Error(`Expected ${path} to exist`);
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(raw) as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── RefEntry: name field on write ───────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("ref.add — identifier field", () => {
|
|
45
|
+
test("single-instance case stores only `ref` (no `name`/`as`)", async () => {
|
|
46
|
+
const fs = createMemoryFs();
|
|
47
|
+
const adk = createAdk(fs);
|
|
48
|
+
|
|
49
|
+
await adk.ref.add({
|
|
50
|
+
ref: "test-ref",
|
|
51
|
+
scheme: "mcp",
|
|
52
|
+
url: "http://localhost:12345",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
56
|
+
fs,
|
|
57
|
+
"consumer-config.json",
|
|
58
|
+
);
|
|
59
|
+
expect(parsed.refs).toHaveLength(1);
|
|
60
|
+
expect(parsed.refs[0].ref).toBe("test-ref");
|
|
61
|
+
expect(parsed.refs[0].name).toBeUndefined();
|
|
62
|
+
expect(parsed.refs[0].as).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("aliasing case stores both `ref` and `name`", async () => {
|
|
66
|
+
const fs = createMemoryFs();
|
|
67
|
+
const adk = createAdk(fs);
|
|
68
|
+
|
|
69
|
+
await adk.ref.add({
|
|
70
|
+
ref: "notion",
|
|
71
|
+
name: "work-notion",
|
|
72
|
+
scheme: "mcp",
|
|
73
|
+
url: "http://localhost:12345",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
77
|
+
fs,
|
|
78
|
+
"consumer-config.json",
|
|
79
|
+
);
|
|
80
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
81
|
+
expect(parsed.refs[0].name).toBe("work-notion");
|
|
82
|
+
// Legacy `as` field is never emitted on new writes.
|
|
83
|
+
expect(parsed.refs[0].as).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── Lookup compatibility: new `name` field works end-to-end ─────────
|
|
88
|
+
|
|
89
|
+
describe("ref lookup — name/as/ref resolution", () => {
|
|
90
|
+
test("entries written with `name` are findable by `name`", async () => {
|
|
91
|
+
const fs = createMemoryFs();
|
|
92
|
+
const adk = createAdk(fs);
|
|
93
|
+
|
|
94
|
+
await adk.ref.add({
|
|
95
|
+
ref: "notion",
|
|
96
|
+
name: "work-notion",
|
|
97
|
+
scheme: "mcp",
|
|
98
|
+
url: "http://localhost:12345",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const entry = await adk.ref.get("work-notion");
|
|
102
|
+
expect(entry).not.toBeNull();
|
|
103
|
+
expect(entry?.ref).toBe("notion");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("entries written with `name` return name field when listed", async () => {
|
|
107
|
+
const fs = createMemoryFs();
|
|
108
|
+
const adk = createAdk(fs);
|
|
109
|
+
|
|
110
|
+
await adk.ref.add({
|
|
111
|
+
ref: "notion",
|
|
112
|
+
name: "work-notion",
|
|
113
|
+
scheme: "mcp",
|
|
114
|
+
url: "http://localhost:12345",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const refs = await adk.ref.list();
|
|
118
|
+
expect(refs).toHaveLength(1);
|
|
119
|
+
expect(refs[0]?.name).toBe("work-notion");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("legacy entries written with `as` remain findable by `as`", async () => {
|
|
123
|
+
// Simulate a consumer-config.json produced by a pre-0.74 client that
|
|
124
|
+
// still writes the `as` field. The read path must still resolve it.
|
|
125
|
+
const fs = createMemoryFs();
|
|
126
|
+
await fs.writeFile(
|
|
127
|
+
"consumer-config.json",
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
refs: [
|
|
130
|
+
{
|
|
131
|
+
ref: "notion",
|
|
132
|
+
as: "work-notion",
|
|
133
|
+
scheme: "mcp",
|
|
134
|
+
url: "http://localhost:12345",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
const adk = createAdk(fs);
|
|
140
|
+
|
|
141
|
+
const entry = await adk.ref.get("work-notion");
|
|
142
|
+
expect(entry).not.toBeNull();
|
|
143
|
+
expect(entry?.ref).toBe("notion");
|
|
144
|
+
expect(entry?.as).toBe("work-notion");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("when both `name` and `as` are present, `name` wins", async () => {
|
|
148
|
+
const fs = createMemoryFs();
|
|
149
|
+
await fs.writeFile(
|
|
150
|
+
"consumer-config.json",
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
refs: [
|
|
153
|
+
{
|
|
154
|
+
ref: "notion",
|
|
155
|
+
name: "new-identifier",
|
|
156
|
+
as: "legacy-identifier",
|
|
157
|
+
scheme: "mcp",
|
|
158
|
+
url: "http://localhost:12345",
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
const adk = createAdk(fs);
|
|
164
|
+
|
|
165
|
+
const byNew = await adk.ref.get("new-identifier");
|
|
166
|
+
expect(byNew).not.toBeNull();
|
|
167
|
+
expect(byNew?.ref).toBe("notion");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── ref.update: renaming clears the legacy `as` field ───────────────
|
|
172
|
+
|
|
173
|
+
describe("ref.update — name/as handling", () => {
|
|
174
|
+
test("passing `name` in updates sets name and clears legacy `as`", async () => {
|
|
175
|
+
const fs = createMemoryFs();
|
|
176
|
+
await fs.writeFile(
|
|
177
|
+
"consumer-config.json",
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
refs: [
|
|
180
|
+
{
|
|
181
|
+
ref: "notion",
|
|
182
|
+
as: "old-alias",
|
|
183
|
+
scheme: "mcp",
|
|
184
|
+
url: "http://localhost:12345",
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
const adk = createAdk(fs);
|
|
190
|
+
|
|
191
|
+
const ok = await adk.ref.update("old-alias", { name: "new-alias" });
|
|
192
|
+
expect(ok).toBe(true);
|
|
193
|
+
|
|
194
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
195
|
+
fs,
|
|
196
|
+
"consumer-config.json",
|
|
197
|
+
);
|
|
198
|
+
expect(parsed.refs[0].name).toBe("new-alias");
|
|
199
|
+
expect(parsed.refs[0].as).toBeUndefined();
|
|
200
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("passing only `as` updates the legacy field (pre-0.74 callers)", async () => {
|
|
204
|
+
const fs = createMemoryFs();
|
|
205
|
+
await fs.writeFile(
|
|
206
|
+
"consumer-config.json",
|
|
207
|
+
JSON.stringify({
|
|
208
|
+
refs: [
|
|
209
|
+
{
|
|
210
|
+
ref: "notion",
|
|
211
|
+
as: "first",
|
|
212
|
+
scheme: "mcp",
|
|
213
|
+
url: "http://localhost:12345",
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
const adk = createAdk(fs);
|
|
219
|
+
|
|
220
|
+
const ok = await adk.ref.update("first", { as: "second" });
|
|
221
|
+
expect(ok).toBe(true);
|
|
222
|
+
|
|
223
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
224
|
+
fs,
|
|
225
|
+
"consumer-config.json",
|
|
226
|
+
);
|
|
227
|
+
expect(parsed.refs[0].as).toBe("second");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─── Tool surface: the LLM non-determinism scenario ──────────────────
|
|
232
|
+
//
|
|
233
|
+
// When an LLM is prompted with "Add a ref called X", its tool-call
|
|
234
|
+
// arguments can land on either `{ ref: 'X', … }` or `{ name: 'X', … }`
|
|
235
|
+
// depending on sampling. Pre-0.74, the former stored `{ ref: 'X' }`
|
|
236
|
+
// and the latter stored `{ ref: undefined }` (broken lookup). The
|
|
237
|
+
// `add` handler now defaults `ref ??= name` so both paths converge on
|
|
238
|
+
// the same stored entry.
|
|
239
|
+
|
|
240
|
+
describe("ref tool — add operation defaults ref to name", () => {
|
|
241
|
+
function makeRefTool(adk: ReturnType<typeof createAdk>) {
|
|
242
|
+
const tools = createAdkTools({ resolveScope: () => adk });
|
|
243
|
+
const ref = tools.find((t) => t.name === "ref");
|
|
244
|
+
if (!ref) throw new Error("ref tool not found");
|
|
245
|
+
return ref;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
test("LLM sends only `name` → ref defaults to name, entry is findable", async () => {
|
|
249
|
+
const fs = createMemoryFs();
|
|
250
|
+
const adk = createAdk(fs);
|
|
251
|
+
const refTool = makeRefTool(adk);
|
|
252
|
+
|
|
253
|
+
const ctx = {} as ToolContext;
|
|
254
|
+
await refTool.execute(
|
|
255
|
+
{
|
|
256
|
+
operation: "add",
|
|
257
|
+
name: "test-identity-ref",
|
|
258
|
+
scheme: "mcp",
|
|
259
|
+
url: "http://127.0.0.1:33469",
|
|
260
|
+
},
|
|
261
|
+
ctx,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
265
|
+
fs,
|
|
266
|
+
"consumer-config.json",
|
|
267
|
+
);
|
|
268
|
+
expect(parsed.refs).toHaveLength(1);
|
|
269
|
+
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
270
|
+
// name was not stored because it equals ref (single-instance case)
|
|
271
|
+
expect(parsed.refs[0].name).toBeUndefined();
|
|
272
|
+
|
|
273
|
+
const entry = await adk.ref.get("test-identity-ref");
|
|
274
|
+
expect(entry).not.toBeNull();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("LLM sends only `ref` → same resulting entry", async () => {
|
|
278
|
+
const fs = createMemoryFs();
|
|
279
|
+
const adk = createAdk(fs);
|
|
280
|
+
const refTool = makeRefTool(adk);
|
|
281
|
+
|
|
282
|
+
const ctx = {} as ToolContext;
|
|
283
|
+
await refTool.execute(
|
|
284
|
+
{
|
|
285
|
+
operation: "add",
|
|
286
|
+
ref: "test-identity-ref",
|
|
287
|
+
scheme: "mcp",
|
|
288
|
+
url: "http://127.0.0.1:33469",
|
|
289
|
+
},
|
|
290
|
+
ctx,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
294
|
+
fs,
|
|
295
|
+
"consumer-config.json",
|
|
296
|
+
);
|
|
297
|
+
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
298
|
+
expect(parsed.refs[0].name).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("LLM sends `ref` + different `name` → stored as canonical + alias", async () => {
|
|
302
|
+
const fs = createMemoryFs();
|
|
303
|
+
const adk = createAdk(fs);
|
|
304
|
+
const refTool = makeRefTool(adk);
|
|
305
|
+
|
|
306
|
+
const ctx = {} as ToolContext;
|
|
307
|
+
await refTool.execute(
|
|
308
|
+
{
|
|
309
|
+
operation: "add",
|
|
310
|
+
ref: "notion",
|
|
311
|
+
name: "work-notion",
|
|
312
|
+
scheme: "mcp",
|
|
313
|
+
url: "http://127.0.0.1:33469",
|
|
314
|
+
},
|
|
315
|
+
ctx,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
319
|
+
fs,
|
|
320
|
+
"consumer-config.json",
|
|
321
|
+
);
|
|
322
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
323
|
+
expect(parsed.refs[0].name).toBe("work-notion");
|
|
324
|
+
|
|
325
|
+
// The entry is findable by the local identifier (name), not the
|
|
326
|
+
// canonical ref.
|
|
327
|
+
expect(await adk.ref.get("work-notion")).not.toBeNull();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("LLM omits both `ref` and `name` → loud error (no silent bad write)", async () => {
|
|
331
|
+
const fs = createMemoryFs();
|
|
332
|
+
const adk = createAdk(fs);
|
|
333
|
+
const refTool = makeRefTool(adk);
|
|
334
|
+
|
|
335
|
+
const ctx = {} as ToolContext;
|
|
336
|
+
await expect(
|
|
337
|
+
refTool.execute(
|
|
338
|
+
{
|
|
339
|
+
operation: "add",
|
|
340
|
+
scheme: "mcp",
|
|
341
|
+
url: "http://127.0.0.1:33469",
|
|
342
|
+
},
|
|
343
|
+
ctx,
|
|
344
|
+
),
|
|
345
|
+
).rejects.toThrow(/ref|name/);
|
|
346
|
+
|
|
347
|
+
// Nothing got written.
|
|
348
|
+
const raw = await fs.readFile("consumer-config.json");
|
|
349
|
+
expect(raw).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|
package/src/registry-consumer.ts
CHANGED
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
ResolvedRegistry,
|
|
40
40
|
} from "./define-config.js";
|
|
41
41
|
import type { CallAgentRequest } from "./call-agent-schema.js";
|
|
42
|
+
import type { FetchFn } from "./fetch-types.js";
|
|
42
43
|
import type { SecuritySchemeSummary, CallAgentResponse } from "./types.js";
|
|
43
44
|
import {
|
|
44
45
|
isSecretUri,
|
|
@@ -331,7 +332,7 @@ async function defaultSecretResolver(
|
|
|
331
332
|
async function listFromMcpServer(
|
|
332
333
|
url: string,
|
|
333
334
|
auth: { token?: string; headers?: Record<string, string> },
|
|
334
|
-
fetchFn:
|
|
335
|
+
fetchFn: FetchFn,
|
|
335
336
|
): Promise<AgentListing[]> {
|
|
336
337
|
const serverUrl = url.replace(/\/$/, "");
|
|
337
338
|
|
|
@@ -409,7 +410,7 @@ function issuerFromMcpUrlAndServerInfo(
|
|
|
409
410
|
async function discoverRegistryViaMcp(
|
|
410
411
|
registryUrl: string,
|
|
411
412
|
authHeaders: Record<string, string>,
|
|
412
|
-
fetchFn:
|
|
413
|
+
fetchFn: FetchFn,
|
|
413
414
|
): Promise<RegistryConfiguration> {
|
|
414
415
|
const serverUrl = registryUrl.replace(/\/$/, "");
|
|
415
416
|
const headers: Record<string, string> = {
|
|
@@ -467,7 +468,7 @@ async function callMcpTool(
|
|
|
467
468
|
toolName: string,
|
|
468
469
|
params: Record<string, unknown>,
|
|
469
470
|
auth: { token?: string; headers?: Record<string, string> },
|
|
470
|
-
fetchFn:
|
|
471
|
+
fetchFn: FetchFn,
|
|
471
472
|
): Promise<unknown> {
|
|
472
473
|
const serverUrl = url.replace(/\/$/, "");
|
|
473
474
|
const headers: Record<string, string> = {
|
|
@@ -539,7 +540,7 @@ async function callHttpsTool(
|
|
|
539
540
|
_toolName: string,
|
|
540
541
|
params: Record<string, unknown>,
|
|
541
542
|
auth: { token?: string; headers?: Record<string, string> },
|
|
542
|
-
fetchFn:
|
|
543
|
+
fetchFn: FetchFn,
|
|
543
544
|
): Promise<unknown> {
|
|
544
545
|
const method = (params.method as string) ?? "GET";
|
|
545
546
|
const path = (params.path as string) ?? "";
|
|
@@ -582,8 +583,13 @@ export interface RegistryConsumerOptions {
|
|
|
582
583
|
/** Bearer token for authenticated registries */
|
|
583
584
|
token?: string;
|
|
584
585
|
|
|
585
|
-
/**
|
|
586
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Custom fetch implementation. Forwarded to every outbound HTTP call the
|
|
588
|
+
* consumer makes (discovery, jwks, callRegistry, secret resolve). Hosts
|
|
589
|
+
* running in long-lived servers should pass a hardened fetch to avoid
|
|
590
|
+
* dead-socket hangs on rolling deploys.
|
|
591
|
+
*/
|
|
592
|
+
fetch?: FetchFn;
|
|
587
593
|
}
|
|
588
594
|
|
|
589
595
|
// ============================================
|
|
@@ -651,7 +657,7 @@ export async function createRegistryConsumer(
|
|
|
651
657
|
config: ConsumerConfig,
|
|
652
658
|
options: RegistryConsumerOptions = {},
|
|
653
659
|
): Promise<RegistryConsumer> {
|
|
654
|
-
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
660
|
+
const fetchFn: FetchFn = options.fetch ?? globalThis.fetch;
|
|
655
661
|
const resolveSecretFn = options.resolveSecret ?? defaultSecretResolver;
|
|
656
662
|
|
|
657
663
|
// Normalize registries
|
package/src/registry.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
8
|
import type { AgentEvent, BaseEvent, CallAgentToolCallEvent, CustomEventMap, EventCallback, EventType, ListAgentsResult, ListAgentsToolCallEvent } from "./events.js";
|
|
9
9
|
import { createEventBus } from "./events.js";
|
|
10
|
+
import type { Logger } from "./logger.js";
|
|
10
11
|
import type { SerializedAgentDefinition } from "./serialized.js";
|
|
11
12
|
import type {
|
|
12
13
|
AgentAction,
|
|
@@ -87,7 +88,11 @@ export interface AgentRegistryOptions {
|
|
|
87
88
|
contextFactory?: ContextFactory;
|
|
88
89
|
/** Lifecycle middleware hooks */
|
|
89
90
|
middleware?: RegistryMiddleware;
|
|
90
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Structured logger for SDK-internal events (listener errors, etc.).
|
|
93
|
+
* Defaults to the module-level default logger (JSON to stdout/stderr).
|
|
94
|
+
*/
|
|
95
|
+
logger?: Logger;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/**
|
|
@@ -231,7 +236,7 @@ export function createAgentRegistry(
|
|
|
231
236
|
): AgentRegistry {
|
|
232
237
|
const { defaultVisibility = "internal" } = options;
|
|
233
238
|
const agents = new Map<string, AgentDefinition>();
|
|
234
|
-
const eventBus = createEventBus();
|
|
239
|
+
const eventBus = createEventBus({ logger: options.logger });
|
|
235
240
|
|
|
236
241
|
/**
|
|
237
242
|
* Check if agent supports the requested action.
|