@slashfi/agents-sdk 0.77.0 → 0.77.2
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 +2 -2
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +258 -118
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +7 -9
- package/dist/adk.js.map +1 -1
- package/dist/agent-definitions/config.d.ts.map +1 -1
- package/dist/agent-definitions/config.js +12 -14
- package/dist/agent-definitions/config.js.map +1 -1
- package/dist/cjs/adk-tools.js +258 -118
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/agent-definitions/config.js +12 -14
- package/dist/cjs/agent-definitions/config.js.map +1 -1
- package/dist/cjs/config-store.js +34 -17
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +5 -7
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/materialize.js +1 -1
- package/dist/cjs/materialize.js.map +1 -1
- package/dist/cjs/registry-consumer.js +1 -1
- package/dist/cjs/registry.js +33 -2
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/config-store.d.ts +3 -3
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +34 -17
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +11 -18
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +5 -7
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/materialize.js +1 -1
- package/dist/materialize.js.map +1 -1
- package/dist/registry-consumer.d.ts +1 -1
- package/dist/registry-consumer.js +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -2
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +289 -127
- package/src/adk.ts +7 -8
- package/src/agent-definitions/config.ts +15 -16
- package/src/config-store.ts +43 -19
- package/src/consumer.test.ts +7 -7
- package/src/define-config.ts +11 -20
- package/src/index.ts +1 -0
- package/src/materialize.ts +1 -1
- package/src/ref-naming.test.ts +164 -91
- package/src/registry-consumer.ts +1 -1
- package/src/registry.ts +40 -2
- package/src/types.ts +21 -6
package/src/config-store.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { FsStore } from "./agent-definitions/config.js";
|
|
21
21
|
import type {
|
|
22
22
|
ConsumerConfig,
|
|
23
|
+
RefAddInput,
|
|
23
24
|
RefEntry,
|
|
24
25
|
RegistryEntry,
|
|
25
26
|
ResolvedRef,
|
|
@@ -248,10 +249,10 @@ type AdkRefCallFn = keyof AdkAgentRegistry extends never
|
|
|
248
249
|
<A extends AgentPath, T extends ToolsOf<A>>(name: A, tool: T, params: ParamsOf<A, T>) => Promise<CallAgentResponse>;
|
|
249
250
|
|
|
250
251
|
export interface AdkRefApi {
|
|
251
|
-
add(entry:
|
|
252
|
+
add(entry: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }>;
|
|
252
253
|
remove(name: string): Promise<boolean>;
|
|
253
254
|
list(): Promise<ResolvedRef[]>;
|
|
254
|
-
get(name: string): Promise<
|
|
255
|
+
get(name: string): Promise<ResolvedRef | null>;
|
|
255
256
|
update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
|
|
256
257
|
inspect(name: string, options?: { full?: boolean }): Promise<AgentInspection | null>;
|
|
257
258
|
call: AdkRefCallFn;
|
|
@@ -332,11 +333,12 @@ function refName(entry: RefEntry): string {
|
|
|
332
333
|
* Refs may be stored as `@foo` or `foo` depending on how they were added;
|
|
333
334
|
* this ensures lookups work regardless of which form the caller uses.
|
|
334
335
|
*/
|
|
335
|
-
function findRef(refs: RefEntry[], name: string):
|
|
336
|
+
function findRef(refs: RefEntry[], name: string): ResolvedRef | undefined {
|
|
336
337
|
const match = refs.find((r) => refName(r) === name);
|
|
337
|
-
if (match) return match;
|
|
338
|
+
if (match) return normalizeRef(match);
|
|
338
339
|
const alt = name.startsWith("@") ? name.slice(1) : `@${name}`;
|
|
339
|
-
|
|
340
|
+
const altMatch = refs.find((r) => refName(r) === alt);
|
|
341
|
+
return altMatch ? normalizeRef(altMatch) : undefined;
|
|
340
342
|
}
|
|
341
343
|
|
|
342
344
|
/**
|
|
@@ -502,7 +504,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
502
504
|
const config = await readConfig();
|
|
503
505
|
const refs = (config.refs ?? []).map((r): RefEntry => {
|
|
504
506
|
if (refName(r) !== name) return r;
|
|
505
|
-
|
|
507
|
+
const normalized = normalizeRef(r);
|
|
508
|
+
return { ...normalized, config: { ...normalized.config, [key]: stored } };
|
|
506
509
|
});
|
|
507
510
|
await writeConfig({ ...config, refs });
|
|
508
511
|
}
|
|
@@ -815,7 +818,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
815
818
|
operation: string,
|
|
816
819
|
params: Record<string, unknown>,
|
|
817
820
|
): Promise<T> {
|
|
818
|
-
const consumer = await buildConsumerForRef({
|
|
821
|
+
const consumer = await buildConsumerForRef({
|
|
822
|
+
ref: "",
|
|
823
|
+
name: "",
|
|
824
|
+
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
825
|
+
});
|
|
819
826
|
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
820
827
|
if (!resolved) throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
|
|
821
828
|
|
|
@@ -1498,11 +1505,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1498
1505
|
// ==========================================
|
|
1499
1506
|
|
|
1500
1507
|
const ref: AdkRefApi = {
|
|
1501
|
-
async add(
|
|
1508
|
+
async add(entryInput: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }> {
|
|
1502
1509
|
let security: SecuritySchemeSummary | null = null;
|
|
1503
1510
|
|
|
1504
1511
|
const config = await readConfig();
|
|
1505
1512
|
const hasRegistries = (config.registries ?? []).length > 0;
|
|
1513
|
+
const name = entryInput.name ?? entryInput.ref;
|
|
1514
|
+
let entry: RefEntry = { ...entryInput, name };
|
|
1515
|
+
|
|
1516
|
+
if ((config.refs ?? []).some((r) => refNameMatches(r, name))) {
|
|
1517
|
+
throw new AdkError({
|
|
1518
|
+
code: "REF_INVALID",
|
|
1519
|
+
message: `Cannot add ref "${entry.ref}" as "${name}": a ref with that name already exists`,
|
|
1520
|
+
hint: "Choose a different name, or remove/update the existing ref first.",
|
|
1521
|
+
details: { ref: entry.ref, name },
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1506
1524
|
|
|
1507
1525
|
// Auto-infer scheme from context
|
|
1508
1526
|
if (!entry.scheme) {
|
|
@@ -1599,9 +1617,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1599
1617
|
}
|
|
1600
1618
|
}
|
|
1601
1619
|
|
|
1602
|
-
const
|
|
1603
|
-
const refs = (config.refs ?? []).filter((r) => refName(r) !== name);
|
|
1604
|
-
refs.push(entry);
|
|
1620
|
+
const refs = [...(config.refs ?? []), entry];
|
|
1605
1621
|
await writeConfig({ ...config, refs });
|
|
1606
1622
|
|
|
1607
1623
|
return { security };
|
|
@@ -1622,7 +1638,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1622
1638
|
return (config.refs ?? []).map(normalizeRef);
|
|
1623
1639
|
},
|
|
1624
1640
|
|
|
1625
|
-
async get(name: string): Promise<
|
|
1641
|
+
async get(name: string): Promise<ResolvedRef | null> {
|
|
1626
1642
|
const config = await readConfig();
|
|
1627
1643
|
return findRef(config.refs ?? [], name) ?? null;
|
|
1628
1644
|
},
|
|
@@ -1636,14 +1652,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1636
1652
|
found = true;
|
|
1637
1653
|
const updated = { ...r };
|
|
1638
1654
|
if (updates.url) updated.url = updates.url;
|
|
1639
|
-
// Rename: prefer `name`, fall back to legacy `as`. When the
|
|
1640
|
-
// caller passes `name`, clear the legacy `as` so the stored
|
|
1641
|
-
// entry has one source of truth.
|
|
1642
1655
|
if (updates.name !== undefined) {
|
|
1656
|
+
const duplicate = config.refs?.some(
|
|
1657
|
+
(candidate) =>
|
|
1658
|
+
!refNameMatches(candidate, name) &&
|
|
1659
|
+
refNameMatches(candidate, updates.name as string),
|
|
1660
|
+
);
|
|
1661
|
+
if (duplicate) {
|
|
1662
|
+
throw new AdkError({
|
|
1663
|
+
code: "REF_INVALID",
|
|
1664
|
+
message: `Cannot rename ref "${name}" to "${updates.name}": a ref with that name already exists`,
|
|
1665
|
+
hint: "Choose a different name, or remove/update the existing ref first.",
|
|
1666
|
+
details: { name, newName: updates.name },
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1643
1669
|
updated.name = updates.name;
|
|
1644
|
-
if (updated.as !== undefined) updated.as = undefined;
|
|
1645
|
-
} else if (updates.as !== undefined) {
|
|
1646
|
-
updated.as = updates.as;
|
|
1647
1670
|
}
|
|
1648
1671
|
if (updates.scheme) updated.scheme = updates.scheme;
|
|
1649
1672
|
if (updates.config) updated.config = { ...updated.config, ...updates.config };
|
|
@@ -2120,7 +2143,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2120
2143
|
// so callers can include extra context (tenant/user IDs).
|
|
2121
2144
|
const statePayload = {
|
|
2122
2145
|
...opts?.stateContext,
|
|
2123
|
-
ref:
|
|
2146
|
+
ref: entry.ref,
|
|
2147
|
+
name,
|
|
2124
2148
|
ts: Date.now(),
|
|
2125
2149
|
};
|
|
2126
2150
|
const state = btoa(JSON.stringify(statePayload));
|
package/src/consumer.test.ts
CHANGED
|
@@ -111,7 +111,7 @@ describe("Registry Consumer E2E", () => {
|
|
|
111
111
|
registries: [`http://localhost:${PORT}`],
|
|
112
112
|
refs: [
|
|
113
113
|
{ ref: "@math" },
|
|
114
|
-
{ ref: "@echo",
|
|
114
|
+
{ ref: "@echo", name: "my-echo", config: { greeting: "hello" } },
|
|
115
115
|
],
|
|
116
116
|
};
|
|
117
117
|
|
|
@@ -151,12 +151,12 @@ describe("Registry Consumer E2E", () => {
|
|
|
151
151
|
);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test("multi-instance refs with
|
|
154
|
+
test("multi-instance refs with name aliases", async () => {
|
|
155
155
|
const config = {
|
|
156
156
|
registries: [`http://localhost:${PORT}`],
|
|
157
157
|
refs: [
|
|
158
|
-
{ ref: "@echo",
|
|
159
|
-
{ ref: "@echo",
|
|
158
|
+
{ ref: "@echo", name: "echo-1", config: { prefix: "first" } },
|
|
159
|
+
{ ref: "@echo", name: "echo-2", config: { prefix: "second" } },
|
|
160
160
|
],
|
|
161
161
|
};
|
|
162
162
|
|
|
@@ -240,10 +240,10 @@ describe("normalizeRef", () => {
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
-
test("object ref with
|
|
243
|
+
test("object ref with name", () => {
|
|
244
244
|
const result = normalizeRef({
|
|
245
245
|
ref: "postgres",
|
|
246
|
-
|
|
246
|
+
name: "prod-db",
|
|
247
247
|
config: { url: "https://twin.slash.com/secrets/db" },
|
|
248
248
|
});
|
|
249
249
|
expect(result.ref).toBe("postgres");
|
|
@@ -253,7 +253,7 @@ describe("normalizeRef", () => {
|
|
|
253
253
|
});
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
-
test("object ref without
|
|
256
|
+
test("object ref without name uses ref as name", () => {
|
|
257
257
|
const result = normalizeRef({ ref: "github" });
|
|
258
258
|
expect(result.name).toBe("github");
|
|
259
259
|
});
|
package/src/define-config.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* ],
|
|
17
17
|
* refs: [
|
|
18
18
|
* 'notion',
|
|
19
|
-
* { ref: 'postgres',
|
|
20
|
-
* { ref: 'postgres',
|
|
19
|
+
* { ref: 'postgres', name: 'prod-db', config: { url: 'https://twin.slash.com/secrets/crdb-url' } },
|
|
20
|
+
* { ref: 'postgres', name: 'staging', config: { url: 'https://twin.slash.com/secrets/staging-url' } },
|
|
21
21
|
* ],
|
|
22
22
|
* });
|
|
23
23
|
* ```
|
|
@@ -158,14 +158,10 @@ export type RefEntry = {
|
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
160
|
* Local identifier for this ref. Used by all operations
|
|
161
|
-
* (call/remove/auth/update/…) to look up the entry.
|
|
162
|
-
*
|
|
163
|
-
* common case "one local instance per agent" requires only
|
|
164
|
-
* `{ ref: 'notion', ... }`. Set `name` to a different value only
|
|
165
|
-
* when you need multiple local instances of the same remote
|
|
166
|
-
* agent (e.g. `{ ref: 'notion', name: 'work-notion' }`).
|
|
161
|
+
* (call/remove/auth/update/…) to look up the entry. Add paths
|
|
162
|
+
* default this to `ref` when omitted.
|
|
167
163
|
*/
|
|
168
|
-
name
|
|
164
|
+
name: string;
|
|
169
165
|
|
|
170
166
|
/** Connection scheme */
|
|
171
167
|
scheme?: 'mcp' | 'https' | 'registry';
|
|
@@ -173,12 +169,6 @@ export type RefEntry = {
|
|
|
173
169
|
/** Direct URL to the agent (e.g. https://mcp.notion.com/mcp) */
|
|
174
170
|
url?: string;
|
|
175
171
|
|
|
176
|
-
/**
|
|
177
|
-
* @deprecated Use `name` instead. `as` is preserved for reading
|
|
178
|
-
* old consumer-config.json files; new writes emit `name`.
|
|
179
|
-
*/
|
|
180
|
-
as?: string;
|
|
181
|
-
|
|
182
172
|
/** Per-instance config (headers, secrets, etc. — values support {{secret-uri}} templates) */
|
|
183
173
|
config?: RefConfig;
|
|
184
174
|
|
|
@@ -189,6 +179,9 @@ export type RefEntry = {
|
|
|
189
179
|
status?: 'active' | 'inactive' | 'error';
|
|
190
180
|
};
|
|
191
181
|
|
|
182
|
+
/** Input accepted by add paths. `name` defaults to `ref` when omitted. */
|
|
183
|
+
export type RefAddInput = Omit<RefEntry, "name"> & { name?: string };
|
|
184
|
+
|
|
192
185
|
// ============================================
|
|
193
186
|
// Consumer Config
|
|
194
187
|
// ============================================
|
|
@@ -259,15 +252,13 @@ export interface ResolvedConfig {
|
|
|
259
252
|
/**
|
|
260
253
|
* Normalize a ref entry to its full form.
|
|
261
254
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* consistent with the `ref.add({ ref, name })` contract while still
|
|
265
|
-
* reading old `{ ref, as }` entries from pre-0.74 consumer-config.json.
|
|
255
|
+
* Add paths default `name` to `ref` before writing. `normalizeRef` keeps the
|
|
256
|
+
* same invariant for in-memory/test configs that omit `name`.
|
|
266
257
|
*/
|
|
267
258
|
export function normalizeRef(entry: RefEntry): ResolvedRef {
|
|
268
259
|
return {
|
|
269
260
|
...entry,
|
|
270
|
-
name: entry.name ?? entry.
|
|
261
|
+
name: entry.name ?? entry.ref,
|
|
271
262
|
config: entry.config ?? {},
|
|
272
263
|
};
|
|
273
264
|
}
|
package/src/index.ts
CHANGED
package/src/materialize.ts
CHANGED
|
@@ -326,7 +326,7 @@ export async function syncAllRefs(
|
|
|
326
326
|
for (const refEntry of refs) {
|
|
327
327
|
const name = typeof refEntry === "string"
|
|
328
328
|
? refEntry
|
|
329
|
-
: (refEntry as any).
|
|
329
|
+
: (refEntry as any).name ?? (refEntry as any).ref;
|
|
330
330
|
if (!name) continue;
|
|
331
331
|
if (opts?.filter && name !== opts.filter) continue;
|
|
332
332
|
names.push(name);
|
package/src/ref-naming.test.ts
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for the `ref` naming contract introduced in 0.74:
|
|
3
3
|
*
|
|
4
|
-
* - `
|
|
5
|
-
*
|
|
6
|
-
* - `
|
|
7
|
-
*
|
|
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.
|
|
4
|
+
* - `name` is the local identifier for every stored ref entry.
|
|
5
|
+
* - Add paths default `name` to `ref` when omitted.
|
|
6
|
+
* - `as` is not part of the stored or public ref shape.
|
|
7
|
+
* - Adding a duplicate `name` is a loud error, not a replace.
|
|
13
8
|
*/
|
|
14
9
|
|
|
15
10
|
import { describe, expect, test } from "bun:test";
|
|
16
11
|
import { createAdkTools } from "./adk-tools";
|
|
17
12
|
import type { FsStore } from "./agent-definitions/config";
|
|
18
|
-
import { createAdk } from "./index";
|
|
13
|
+
import { createAdk, createAgentRegistry, defineAgent } from "./index";
|
|
19
14
|
import type { ToolContext } from "./types";
|
|
20
15
|
|
|
21
16
|
function createMemoryFs(): FsStore {
|
|
@@ -42,7 +37,7 @@ async function readJson<T = unknown>(fs: FsStore, path: string): Promise<T> {
|
|
|
42
37
|
// ─── RefEntry: name field on write ───────────────────────────────────
|
|
43
38
|
|
|
44
39
|
describe("ref.add — identifier field", () => {
|
|
45
|
-
test("single-instance case stores
|
|
40
|
+
test("single-instance case stores explicit `name` equal to `ref`", async () => {
|
|
46
41
|
const fs = createMemoryFs();
|
|
47
42
|
const adk = createAdk(fs);
|
|
48
43
|
|
|
@@ -58,8 +53,7 @@ describe("ref.add — identifier field", () => {
|
|
|
58
53
|
);
|
|
59
54
|
expect(parsed.refs).toHaveLength(1);
|
|
60
55
|
expect(parsed.refs[0].ref).toBe("test-ref");
|
|
61
|
-
expect(parsed.refs[0].name).
|
|
62
|
-
expect(parsed.refs[0].as).toBeUndefined();
|
|
56
|
+
expect(parsed.refs[0].name).toBe("test-ref");
|
|
63
57
|
});
|
|
64
58
|
|
|
65
59
|
test("aliasing case stores both `ref` and `name`", async () => {
|
|
@@ -79,14 +73,40 @@ describe("ref.add — identifier field", () => {
|
|
|
79
73
|
);
|
|
80
74
|
expect(parsed.refs[0].ref).toBe("notion");
|
|
81
75
|
expect(parsed.refs[0].name).toBe("work-notion");
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("adding a duplicate name rejects instead of replacing", async () => {
|
|
79
|
+
const fs = createMemoryFs();
|
|
80
|
+
const adk = createAdk(fs);
|
|
81
|
+
|
|
82
|
+
await adk.ref.add({
|
|
83
|
+
ref: "notion",
|
|
84
|
+
name: "work",
|
|
85
|
+
scheme: "mcp",
|
|
86
|
+
url: "http://localhost:12345",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await expect(
|
|
90
|
+
adk.ref.add({
|
|
91
|
+
ref: "linear",
|
|
92
|
+
name: "work",
|
|
93
|
+
scheme: "mcp",
|
|
94
|
+
url: "http://localhost:12346",
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toThrow(/already exists/);
|
|
97
|
+
|
|
98
|
+
const parsed = await readJson<{ refs: Array<Record<string, unknown>> }>(
|
|
99
|
+
fs,
|
|
100
|
+
"consumer-config.json",
|
|
101
|
+
);
|
|
102
|
+
expect(parsed.refs).toHaveLength(1);
|
|
103
|
+
expect(parsed.refs[0].ref).toBe("notion");
|
|
84
104
|
});
|
|
85
105
|
});
|
|
86
106
|
|
|
87
107
|
// ─── Lookup compatibility: new `name` field works end-to-end ─────────
|
|
88
108
|
|
|
89
|
-
describe("ref lookup — name/
|
|
109
|
+
describe("ref lookup — name/ref resolution", () => {
|
|
90
110
|
test("entries written with `name` are findable by `name`", async () => {
|
|
91
111
|
const fs = createMemoryFs();
|
|
92
112
|
const adk = createAdk(fs);
|
|
@@ -119,9 +139,7 @@ describe("ref lookup — name/as/ref resolution", () => {
|
|
|
119
139
|
expect(refs[0]?.name).toBe("work-notion");
|
|
120
140
|
});
|
|
121
141
|
|
|
122
|
-
test("
|
|
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.
|
|
142
|
+
test("entries without `name` normalize to `ref`", async () => {
|
|
125
143
|
const fs = createMemoryFs();
|
|
126
144
|
await fs.writeFile(
|
|
127
145
|
"consumer-config.json",
|
|
@@ -129,7 +147,6 @@ describe("ref lookup — name/as/ref resolution", () => {
|
|
|
129
147
|
refs: [
|
|
130
148
|
{
|
|
131
149
|
ref: "notion",
|
|
132
|
-
as: "work-notion",
|
|
133
150
|
scheme: "mcp",
|
|
134
151
|
url: "http://localhost:12345",
|
|
135
152
|
},
|
|
@@ -138,56 +155,26 @@ describe("ref lookup — name/as/ref resolution", () => {
|
|
|
138
155
|
);
|
|
139
156
|
const adk = createAdk(fs);
|
|
140
157
|
|
|
141
|
-
const
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
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");
|
|
158
|
+
const refs = await adk.ref.list();
|
|
159
|
+
expect(refs[0]?.name).toBe("notion");
|
|
160
|
+
expect(await adk.ref.get("notion")).not.toBeNull();
|
|
168
161
|
});
|
|
169
162
|
});
|
|
170
163
|
|
|
171
|
-
// ─── ref.update:
|
|
164
|
+
// ─── ref.update: name is the only rename field ───────────────────────
|
|
172
165
|
|
|
173
|
-
describe("ref.update — name
|
|
174
|
-
test("passing `name` in updates sets name
|
|
166
|
+
describe("ref.update — name handling", () => {
|
|
167
|
+
test("passing `name` in updates sets name", async () => {
|
|
175
168
|
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
169
|
const adk = createAdk(fs);
|
|
190
170
|
|
|
171
|
+
await adk.ref.add({
|
|
172
|
+
ref: "notion",
|
|
173
|
+
name: "old-alias",
|
|
174
|
+
scheme: "mcp",
|
|
175
|
+
url: "http://localhost:12345",
|
|
176
|
+
});
|
|
177
|
+
|
|
191
178
|
const ok = await adk.ref.update("old-alias", { name: "new-alias" });
|
|
192
179
|
expect(ok).toBe(true);
|
|
193
180
|
|
|
@@ -196,35 +183,29 @@ describe("ref.update — name/as handling", () => {
|
|
|
196
183
|
"consumer-config.json",
|
|
197
184
|
);
|
|
198
185
|
expect(parsed.refs[0].name).toBe("new-alias");
|
|
199
|
-
expect(parsed.refs[0].as).toBeUndefined();
|
|
200
186
|
expect(parsed.refs[0].ref).toBe("notion");
|
|
201
187
|
});
|
|
202
188
|
|
|
203
|
-
test("
|
|
189
|
+
test("renaming to an existing name rejects", async () => {
|
|
204
190
|
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
191
|
const adk = createAdk(fs);
|
|
219
192
|
|
|
220
|
-
|
|
221
|
-
|
|
193
|
+
await adk.ref.add({
|
|
194
|
+
ref: "notion",
|
|
195
|
+
name: "first",
|
|
196
|
+
scheme: "mcp",
|
|
197
|
+
url: "http://localhost:12345",
|
|
198
|
+
});
|
|
199
|
+
await adk.ref.add({
|
|
200
|
+
ref: "linear",
|
|
201
|
+
name: "second",
|
|
202
|
+
scheme: "mcp",
|
|
203
|
+
url: "http://localhost:12346",
|
|
204
|
+
});
|
|
222
205
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"consumer-config.json",
|
|
206
|
+
await expect(adk.ref.update("first", { name: "second" })).rejects.toThrow(
|
|
207
|
+
/already exists/,
|
|
226
208
|
);
|
|
227
|
-
expect(parsed.refs[0].as).toBe("second");
|
|
228
209
|
});
|
|
229
210
|
});
|
|
230
211
|
|
|
@@ -232,10 +213,8 @@ describe("ref.update — name/as handling", () => {
|
|
|
232
213
|
//
|
|
233
214
|
// When an LLM is prompted with "Add a ref called X", its tool-call
|
|
234
215
|
// arguments can land on either `{ ref: 'X', … }` or `{ name: 'X', … }`
|
|
235
|
-
// depending on sampling.
|
|
236
|
-
//
|
|
237
|
-
// `add` handler now defaults `ref ??= name` so both paths converge on
|
|
238
|
-
// the same stored entry.
|
|
216
|
+
// depending on sampling. The `add` handler defaults the missing field so
|
|
217
|
+
// both paths converge on the same stored `{ ref: 'X', name: 'X' }` entry.
|
|
239
218
|
|
|
240
219
|
describe("ref tool — add operation defaults ref to name", () => {
|
|
241
220
|
function makeRefTool(adk: ReturnType<typeof createAdk>) {
|
|
@@ -267,8 +246,7 @@ describe("ref tool — add operation defaults ref to name", () => {
|
|
|
267
246
|
);
|
|
268
247
|
expect(parsed.refs).toHaveLength(1);
|
|
269
248
|
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
270
|
-
|
|
271
|
-
expect(parsed.refs[0].name).toBeUndefined();
|
|
249
|
+
expect(parsed.refs[0].name).toBe("test-identity-ref");
|
|
272
250
|
|
|
273
251
|
const entry = await adk.ref.get("test-identity-ref");
|
|
274
252
|
expect(entry).not.toBeNull();
|
|
@@ -295,7 +273,7 @@ describe("ref tool — add operation defaults ref to name", () => {
|
|
|
295
273
|
"consumer-config.json",
|
|
296
274
|
);
|
|
297
275
|
expect(parsed.refs[0].ref).toBe("test-identity-ref");
|
|
298
|
-
expect(parsed.refs[0].name).
|
|
276
|
+
expect(parsed.refs[0].name).toBe("test-identity-ref");
|
|
299
277
|
});
|
|
300
278
|
|
|
301
279
|
test("LLM sends `ref` + different `name` → stored as canonical + alias", async () => {
|
|
@@ -348,4 +326,99 @@ describe("ref tool — add operation defaults ref to name", () => {
|
|
|
348
326
|
const raw = await fs.readFile("consumer-config.json");
|
|
349
327
|
expect(raw).toBeNull();
|
|
350
328
|
});
|
|
329
|
+
|
|
330
|
+
test("invalid add input returns schema details through registry call", async () => {
|
|
331
|
+
const fs = createMemoryFs();
|
|
332
|
+
const adk = createAdk(fs);
|
|
333
|
+
const refTool = makeRefTool(adk);
|
|
334
|
+
const registry = createAgentRegistry();
|
|
335
|
+
registry.register(
|
|
336
|
+
defineAgent({
|
|
337
|
+
path: "@config",
|
|
338
|
+
entrypoint: "Config agent",
|
|
339
|
+
tools: [refTool],
|
|
340
|
+
visibility: "public",
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const response = await registry.call({
|
|
345
|
+
action: "execute_tool",
|
|
346
|
+
path: "@config",
|
|
347
|
+
tool: "ref",
|
|
348
|
+
params: {
|
|
349
|
+
operation: "add",
|
|
350
|
+
ref: "google-calendar",
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
expect(response.success).toBe(false);
|
|
355
|
+
if (response.success) throw new Error("expected invalid input error");
|
|
356
|
+
expect(response.code).toBe("TOOL_INPUT_INVALID");
|
|
357
|
+
expect(response.error).toContain("Invalid ref.add input");
|
|
358
|
+
expect(response.details?.issues).toEqual(
|
|
359
|
+
expect.arrayContaining([
|
|
360
|
+
expect.objectContaining({
|
|
361
|
+
path: "sourceRegistry",
|
|
362
|
+
}),
|
|
363
|
+
]),
|
|
364
|
+
);
|
|
365
|
+
expect(response.details?.schema).toMatchObject({
|
|
366
|
+
anyOf: expect.any(Array),
|
|
367
|
+
});
|
|
368
|
+
expect(response.details?.operationSchema).toMatchObject({
|
|
369
|
+
type: "object",
|
|
370
|
+
});
|
|
371
|
+
expect(response.hint).toContain("details.schema");
|
|
372
|
+
expect(response.details).not.toHaveProperty("examples");
|
|
373
|
+
expect(JSON.stringify(response.details?.operationSchema)).toContain(
|
|
374
|
+
"sourceRegistry",
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("ref tool — auth state hook", () => {
|
|
380
|
+
test("passes tool input to getAuthStateContext", async () => {
|
|
381
|
+
const authCalls: Array<{
|
|
382
|
+
name: string;
|
|
383
|
+
opts: { stateContext?: Record<string, unknown> };
|
|
384
|
+
}> = [];
|
|
385
|
+
const adk = {
|
|
386
|
+
ref: {
|
|
387
|
+
auth: async (
|
|
388
|
+
name: string,
|
|
389
|
+
opts: { stateContext?: Record<string, unknown> },
|
|
390
|
+
) => {
|
|
391
|
+
authCalls.push({ name, opts });
|
|
392
|
+
return { complete: true };
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
} as unknown as ReturnType<typeof createAdk>;
|
|
396
|
+
const tools = createAdkTools({
|
|
397
|
+
resolveScope: () => adk,
|
|
398
|
+
hooks: {
|
|
399
|
+
getAuthStateContext: async (input) => ({
|
|
400
|
+
name: input.name,
|
|
401
|
+
ref: input.ref,
|
|
402
|
+
}),
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
const refTool = tools.find((t) => t.name === "ref");
|
|
406
|
+
if (!refTool) throw new Error("ref tool not found");
|
|
407
|
+
|
|
408
|
+
await refTool.execute(
|
|
409
|
+
{
|
|
410
|
+
operation: "auth",
|
|
411
|
+
ref: "google-gmail",
|
|
412
|
+
name: "work2",
|
|
413
|
+
},
|
|
414
|
+
{} as ToolContext,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
expect(authCalls).toHaveLength(1);
|
|
418
|
+
expect(authCalls[0]?.name).toBe("work2");
|
|
419
|
+
expect(authCalls[0]?.opts.stateContext).toEqual({
|
|
420
|
+
ref: "google-gmail",
|
|
421
|
+
name: "work2",
|
|
422
|
+
});
|
|
423
|
+
});
|
|
351
424
|
});
|
package/src/registry-consumer.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* const config = defineConfig({
|
|
14
14
|
* registries: ['https://registry.slash.com'],
|
|
15
|
-
* refs: ['notion', { ref: 'postgres',
|
|
15
|
+
* refs: ['notion', { ref: 'postgres', name: 'prod-db', config: { url: '...' } }],
|
|
16
16
|
* });
|
|
17
17
|
*
|
|
18
18
|
* const consumer = await createRegistryConsumer(config);
|