@slashfi/agents-sdk 0.77.3 → 0.78.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.
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import type { FsStore } from "./agent-definitions/config";
2
3
  import {
3
4
  createAdk,
4
5
  createAdkTools,
@@ -8,7 +9,6 @@ import {
8
9
  defineTool,
9
10
  } from "./index";
10
11
  import type { AgentServer } from "./index";
11
- import type { FsStore } from "./agent-definitions/config";
12
12
 
13
13
  // ─── Helpers ─────────────────────────────────────────────────────
14
14
 
@@ -146,7 +146,8 @@ describe("ADK ref sourceRegistry routing", () => {
146
146
  // Inspect should find the agent on the source server
147
147
  const info = await adk.ref.inspect("@math");
148
148
  expect(info).toBeDefined();
149
- const toolCount = (info?.tools?.length ?? 0) + (info?.toolSummaries?.length ?? 0);
149
+ const toolCount =
150
+ (info?.tools?.length ?? 0) + (info?.toolSummaries?.length ?? 0);
150
151
  expect(toolCount).toBeGreaterThan(0);
151
152
  });
152
153
  });
@@ -158,9 +159,9 @@ describe("ADK ref.add validation", () => {
158
159
  const fs = createMemoryFs();
159
160
  const adk = createAdk(fs);
160
161
 
161
- await expect(
162
- adk.ref.add({ ref: "@something" }),
163
- ).rejects.toThrow("could not determine connection type");
162
+ await expect(adk.ref.add({ ref: "@something" })).rejects.toThrow(
163
+ "could not determine connection type",
164
+ );
164
165
  });
165
166
 
166
167
  test("throws when scheme is 'registry' without sourceRegistry", async () => {
@@ -254,7 +255,10 @@ describe("ADK ref.call() auto-refresh on 401", () => {
254
255
  execute: async () => {
255
256
  callCount++;
256
257
  if (callCount === 1) {
257
- return { content: [{ type: "text", text: '{"error":"401 Unauthorized"}' }], _httpStatus: 401 };
258
+ return {
259
+ content: [{ type: "text", text: '{"error":"401 Unauthorized"}' }],
260
+ _httpStatus: 401,
261
+ };
258
262
  }
259
263
  return { data: "success", callNumber: callCount };
260
264
  },
@@ -352,19 +356,29 @@ describe("ADK ref.call() full auto-refresh flow", () => {
352
356
  const body = await req.text();
353
357
  const params = new URLSearchParams(body);
354
358
  if (params.get("grant_type") !== "refresh_token") {
355
- return new Response(JSON.stringify({ error: "unsupported_grant_type" }), { status: 400 });
359
+ return new Response(
360
+ JSON.stringify({ error: "unsupported_grant_type" }),
361
+ { status: 400 },
362
+ );
356
363
  }
357
364
  if (params.get("refresh_token") !== "my-refresh-token") {
358
- return new Response(JSON.stringify({ error: "invalid_grant" }), { status: 400 });
365
+ return new Response(JSON.stringify({ error: "invalid_grant" }), {
366
+ status: 400,
367
+ });
359
368
  }
360
369
  if (params.get("client_id") !== "my-client-id") {
361
- return new Response(JSON.stringify({ error: "invalid_client" }), { status: 401 });
370
+ return new Response(JSON.stringify({ error: "invalid_client" }), {
371
+ status: 401,
372
+ });
362
373
  }
363
- return new Response(JSON.stringify({
364
- access_token: "refreshed-token",
365
- token_type: "Bearer",
366
- expires_in: 3600,
367
- }), { headers: { "Content-Type": "application/json" } });
374
+ return new Response(
375
+ JSON.stringify({
376
+ access_token: "refreshed-token",
377
+ token_type: "Bearer",
378
+ expires_in: 3600,
379
+ }),
380
+ { headers: { "Content-Type": "application/json" } },
381
+ );
368
382
  },
369
383
  });
370
384
 
@@ -377,12 +391,18 @@ describe("ADK ref.call() full auto-refresh flow", () => {
377
391
  toolCallCount++;
378
392
  const token = input?.accessToken;
379
393
  if (token === "expired-token" || !token) {
380
- return { content: [{ type: "text", text: '{"error":"401 Unauthorized"}' }], _httpStatus: 401 };
394
+ return {
395
+ content: [{ type: "text", text: '{"error":"401 Unauthorized"}' }],
396
+ _httpStatus: 401,
397
+ };
381
398
  }
382
399
  if (token === "refreshed-token") {
383
400
  return { message: "success", token };
384
401
  }
385
- return { content: [{ type: "text", text: '{"error":"403 Forbidden"}' }], _httpStatus: 403 };
402
+ return {
403
+ content: [{ type: "text", text: '{"error":"403 Forbidden"}' }],
404
+ _httpStatus: 403,
405
+ };
386
406
  },
387
407
  });
388
408
 
@@ -420,12 +440,20 @@ describe("ADK ref.call() full auto-refresh flow", () => {
420
440
  tokenRefreshCount = 0;
421
441
 
422
442
  const fs = createMemoryFs();
423
- const adk = createAdk(fs, { encryptionKey: "test-key-32-chars-long-enough!!" });
443
+ const adk = createAdk(fs, {
444
+ encryptionKey: "test-key-32-chars-long-enough!!",
445
+ });
424
446
 
425
- await adk.registry.add({ name: "oauth-reg", url: `http://localhost:${REG_PORT}` });
447
+ await adk.registry.add({
448
+ name: "oauth-reg",
449
+ url: `http://localhost:${REG_PORT}`,
450
+ });
426
451
  await adk.ref.add({
427
452
  ref: "oauth-api",
428
- sourceRegistry: { url: `http://localhost:${REG_PORT}`, agentPath: "oauth-api" },
453
+ sourceRegistry: {
454
+ url: `http://localhost:${REG_PORT}`,
455
+ agentPath: "oauth-api",
456
+ },
429
457
  });
430
458
 
431
459
  // Store credentials directly
@@ -599,7 +627,9 @@ describe("ADK registry proxy routing", () => {
599
627
  proxy: { mode: "optional", agent: "@custom" },
600
628
  });
601
629
 
602
- const entry = (await adk.registry.list()).find((r) => r.name === "cloud-explicit");
630
+ const entry = (await adk.registry.list()).find(
631
+ (r) => r.name === "cloud-explicit",
632
+ );
603
633
  expect(entry?.proxy?.mode).toBe("optional");
604
634
  expect(entry?.proxy?.agent).toBe("@custom");
605
635
  });
@@ -694,10 +724,9 @@ describe("ADK registry auth lifecycle", () => {
694
724
  if (grant === "refresh_token") {
695
725
  tokenRefreshCount++;
696
726
  if (body.get("refresh_token") !== "refresh-token-v1") {
697
- return new Response(
698
- JSON.stringify({ error: "invalid_grant" }),
699
- { status: 400 },
700
- );
727
+ return new Response(JSON.stringify({ error: "invalid_grant" }), {
728
+ status: 400,
729
+ });
701
730
  }
702
731
  // Rotate to a new access token so the test can tell refresh ran.
703
732
  activeAccessToken = "access-token-v2";
@@ -716,7 +745,9 @@ describe("ADK registry auth lifecycle", () => {
716
745
  const expected = `Bearer ${activeAccessToken}`;
717
746
  if (auth !== expected) {
718
747
  return new Response(
719
- JSON.stringify({ error: { code: "UNAUTHORIZED", message: "No token" } }),
748
+ JSON.stringify({
749
+ error: { code: "UNAUTHORIZED", message: "No token" },
750
+ }),
720
751
  {
721
752
  status: 401,
722
753
  headers: {
@@ -744,7 +775,11 @@ describe("ADK registry auth lifecycle", () => {
744
775
  type: "text",
745
776
  text: JSON.stringify({
746
777
  agents: [
747
- { path: "@test-agent", description: "An agent", toolCount: 1 },
778
+ {
779
+ path: "@test-agent",
780
+ description: "An agent",
781
+ toolCount: 1,
782
+ },
748
783
  ],
749
784
  }),
750
785
  },
@@ -766,7 +801,9 @@ describe("ADK registry auth lifecycle", () => {
766
801
 
767
802
  test("registry.add records auth challenge; browse refuses; auth() unlocks", async () => {
768
803
  const fs = createMemoryFs();
769
- const adk = createAdk(fs, { encryptionKey: "test-key-32-chars-long-enough!!" });
804
+ const adk = createAdk(fs, {
805
+ encryptionKey: "test-key-32-chars-long-enough!!",
806
+ });
770
807
 
771
808
  const addResult = await adk.registry.add({ name: "test", url: MCP_URL });
772
809
 
@@ -795,7 +832,9 @@ describe("ADK registry auth lifecycle", () => {
795
832
 
796
833
  test("browse 401 triggers refresh via stored refresh_token and retries", async () => {
797
834
  const fs = createMemoryFs();
798
- const adk = createAdk(fs, { encryptionKey: "test-key-32-chars-long-enough!!" });
835
+ const adk = createAdk(fs, {
836
+ encryptionKey: "test-key-32-chars-long-enough!!",
837
+ });
799
838
 
800
839
  // Reset server-side token so the next refresh rotates predictably.
801
840
  activeAccessToken = "access-token-v1";
@@ -838,3 +877,281 @@ describe("ADK registry auth lifecycle", () => {
838
877
  expect((stored?.auth as { token: string }).token).toMatch(/^secret:/);
839
878
  });
840
879
  });
880
+
881
+ // ─── ADK ref registry cache ──────────────────────────────────────
882
+
883
+ describe("ADK ref registry cache", () => {
884
+ let server: AgentServer;
885
+ const PORT = 19940;
886
+
887
+ // Agents configured with descriptions so the registry's describe_tools
888
+ // response carries metadata for the cache to capture.
889
+ const cachedMathAgent = defineAgent({
890
+ path: "@cached-math",
891
+ entrypoint: "Math agent",
892
+ tools: [add],
893
+ visibility: "public",
894
+ config: { description: "Adds numbers together" },
895
+ });
896
+
897
+ const cachedEchoAgent = defineAgent({
898
+ path: "@cached-echo",
899
+ entrypoint: "Echo agent",
900
+ tools: [echo],
901
+ visibility: "public",
902
+ config: { description: "Echoes back the input" },
903
+ });
904
+
905
+ beforeAll(async () => {
906
+ const registry = createAgentRegistry();
907
+ registry.register(cachedMathAgent);
908
+ registry.register(cachedEchoAgent);
909
+ server = createAgentServer(registry, { port: PORT });
910
+ await server.start();
911
+ });
912
+
913
+ afterAll(async () => {
914
+ await server.stop();
915
+ });
916
+
917
+ test("ref.add populates registry-cache.json with description and slim tools", async () => {
918
+ const fs = createMemoryFs();
919
+ const adk = createAdk(fs);
920
+
921
+ await adk.registry.add({
922
+ url: `http://localhost:${PORT}`,
923
+ name: "main",
924
+ });
925
+ await adk.ref.add({
926
+ ref: "@cached-math",
927
+ scheme: "registry",
928
+ sourceRegistry: {
929
+ url: `http://localhost:${PORT}`,
930
+ agentPath: "@cached-math",
931
+ },
932
+ });
933
+
934
+ const cacheRaw = await fs.readFile("registry-cache.json");
935
+ expect(cacheRaw).not.toBeNull();
936
+ const cache = JSON.parse(cacheRaw!);
937
+ const entry = cache.refs["@cached-math"];
938
+ expect(entry).toBeDefined();
939
+ expect(entry.ref).toBe("@cached-math");
940
+ expect(entry.description).toBe("Adds numbers together");
941
+ expect(entry.tools).toBeDefined();
942
+ expect(entry.tools.length).toBeGreaterThan(0);
943
+ expect(entry.tools[0].name).toBe("add");
944
+ expect(entry.tools[0].description).toBe("Add two numbers");
945
+ // inputSchema MUST NOT leak into the cache — that's our whole point.
946
+ expect(entry.tools[0]).not.toHaveProperty("inputSchema");
947
+ expect(typeof entry.fetchedAt).toBe("string");
948
+ });
949
+
950
+ test("ref.list hydrates description and tools from cache", async () => {
951
+ const fs = createMemoryFs();
952
+ const adk = createAdk(fs);
953
+
954
+ await adk.registry.add({
955
+ url: `http://localhost:${PORT}`,
956
+ name: "main",
957
+ });
958
+ await adk.ref.add({
959
+ ref: "@cached-math",
960
+ scheme: "registry",
961
+ sourceRegistry: {
962
+ url: `http://localhost:${PORT}`,
963
+ agentPath: "@cached-math",
964
+ },
965
+ });
966
+
967
+ const refs = await adk.ref.list();
968
+ expect(refs).toHaveLength(1);
969
+ expect(refs[0].name).toBe("@cached-math");
970
+ expect(refs[0].description).toBe("Adds numbers together");
971
+ expect(refs[0].tools).toBeDefined();
972
+ expect(refs[0].tools?.[0].name).toBe("add");
973
+ });
974
+
975
+ test("ref.list returns description undefined when cache is empty", async () => {
976
+ const fs = createMemoryFs();
977
+ // Seed a ref directly into consumer-config without a cache entry — this is
978
+ // the "existing user, fresh cache" case (e.g. before the backfill runs).
979
+ await fs.writeFile(
980
+ "consumer-config.json",
981
+ JSON.stringify({
982
+ refs: [
983
+ {
984
+ ref: "@cached-math",
985
+ name: "@cached-math",
986
+ scheme: "registry",
987
+ sourceRegistry: {
988
+ url: `http://localhost:${PORT}`,
989
+ agentPath: "@cached-math",
990
+ },
991
+ },
992
+ ],
993
+ }),
994
+ );
995
+
996
+ const adk = createAdk(fs);
997
+ const refs = await adk.ref.list();
998
+ expect(refs).toHaveLength(1);
999
+ expect(refs[0].description).toBeUndefined();
1000
+ expect(refs[0].tools).toBeUndefined();
1001
+ });
1002
+
1003
+ test("ref.get hydrates a single ref from the cache", async () => {
1004
+ const fs = createMemoryFs();
1005
+ const adk = createAdk(fs);
1006
+
1007
+ await adk.registry.add({
1008
+ url: `http://localhost:${PORT}`,
1009
+ name: "main",
1010
+ });
1011
+ await adk.ref.add({
1012
+ ref: "@cached-math",
1013
+ scheme: "registry",
1014
+ sourceRegistry: {
1015
+ url: `http://localhost:${PORT}`,
1016
+ agentPath: "@cached-math",
1017
+ },
1018
+ });
1019
+
1020
+ const ref = await adk.ref.get("@cached-math");
1021
+ expect(ref).not.toBeNull();
1022
+ expect(ref?.description).toBe("Adds numbers together");
1023
+ expect(ref?.tools?.[0].name).toBe("add");
1024
+ });
1025
+
1026
+ test("ref.inspect refreshes the cache for the inspected ref", async () => {
1027
+ const fs = createMemoryFs();
1028
+ const adk = createAdk(fs);
1029
+
1030
+ await adk.registry.add({
1031
+ url: `http://localhost:${PORT}`,
1032
+ name: "main",
1033
+ });
1034
+ // Seed without registry add-time inspect (use bare config seeding) so the
1035
+ // cache starts empty and we can see ref.inspect populate it.
1036
+ await fs.writeFile(
1037
+ "consumer-config.json",
1038
+ JSON.stringify({
1039
+ registries: [{ url: `http://localhost:${PORT}`, name: "main" }],
1040
+ refs: [
1041
+ {
1042
+ ref: "@cached-math",
1043
+ name: "@cached-math",
1044
+ scheme: "registry",
1045
+ sourceRegistry: {
1046
+ url: `http://localhost:${PORT}`,
1047
+ agentPath: "@cached-math",
1048
+ },
1049
+ },
1050
+ ],
1051
+ }),
1052
+ );
1053
+
1054
+ // Cache is empty before inspect.
1055
+ const beforeRefs = await adk.ref.list();
1056
+ expect(beforeRefs[0].description).toBeUndefined();
1057
+
1058
+ // Inspect populates the cache.
1059
+ const info = await adk.ref.inspect("@cached-math");
1060
+ expect(info).toBeDefined();
1061
+
1062
+ const afterRefs = await adk.ref.list();
1063
+ expect(afterRefs[0].description).toBe("Adds numbers together");
1064
+ expect(afterRefs[0].tools?.[0].name).toBe("add");
1065
+ });
1066
+
1067
+ test("ref.inspect with full: true does not leak inputSchema into the cache", async () => {
1068
+ const fs = createMemoryFs();
1069
+ const adk = createAdk(fs);
1070
+
1071
+ await adk.registry.add({
1072
+ url: `http://localhost:${PORT}`,
1073
+ name: "main",
1074
+ });
1075
+ await adk.ref.add({
1076
+ ref: "@cached-math",
1077
+ scheme: "registry",
1078
+ sourceRegistry: {
1079
+ url: `http://localhost:${PORT}`,
1080
+ agentPath: "@cached-math",
1081
+ },
1082
+ });
1083
+
1084
+ // Caller gets the full schema in the response…
1085
+ const full = await adk.ref.inspect("@cached-math", { full: true });
1086
+ expect(full?.tools?.[0]).toHaveProperty("inputSchema");
1087
+
1088
+ // …but the cache stays slim.
1089
+ const cacheRaw = await fs.readFile("registry-cache.json");
1090
+ const cache = JSON.parse(cacheRaw!);
1091
+ const entry = cache.refs["@cached-math"];
1092
+ expect(entry.tools[0]).not.toHaveProperty("inputSchema");
1093
+ });
1094
+
1095
+ test("ref.remove drops the cache entry", async () => {
1096
+ const fs = createMemoryFs();
1097
+ const adk = createAdk(fs);
1098
+
1099
+ await adk.registry.add({
1100
+ url: `http://localhost:${PORT}`,
1101
+ name: "main",
1102
+ });
1103
+ await adk.ref.add({
1104
+ ref: "@cached-math",
1105
+ scheme: "registry",
1106
+ sourceRegistry: {
1107
+ url: `http://localhost:${PORT}`,
1108
+ agentPath: "@cached-math",
1109
+ },
1110
+ });
1111
+ await adk.ref.add({
1112
+ ref: "@cached-echo",
1113
+ scheme: "registry",
1114
+ sourceRegistry: {
1115
+ url: `http://localhost:${PORT}`,
1116
+ agentPath: "@cached-echo",
1117
+ },
1118
+ });
1119
+
1120
+ let cache = JSON.parse((await fs.readFile("registry-cache.json"))!);
1121
+ expect(Object.keys(cache.refs)).toEqual(
1122
+ expect.arrayContaining(["@cached-math", "@cached-echo"]),
1123
+ );
1124
+
1125
+ await adk.ref.remove("@cached-math");
1126
+
1127
+ cache = JSON.parse((await fs.readFile("registry-cache.json"))!);
1128
+ expect(cache.refs["@cached-math"]).toBeUndefined();
1129
+ expect(cache.refs["@cached-echo"]).toBeDefined();
1130
+ });
1131
+
1132
+ test("malformed registry-cache.json is treated as empty (does not break list)", async () => {
1133
+ const fs = createMemoryFs();
1134
+ await fs.writeFile(
1135
+ "consumer-config.json",
1136
+ JSON.stringify({
1137
+ refs: [
1138
+ {
1139
+ ref: "@cached-math",
1140
+ name: "@cached-math",
1141
+ scheme: "registry",
1142
+ sourceRegistry: {
1143
+ url: `http://localhost:${PORT}`,
1144
+ agentPath: "@cached-math",
1145
+ },
1146
+ },
1147
+ ],
1148
+ }),
1149
+ );
1150
+ await fs.writeFile("registry-cache.json", "{ this is not json");
1151
+
1152
+ const adk = createAdk(fs);
1153
+ const refs = await adk.ref.list();
1154
+ expect(refs).toHaveLength(1);
1155
+ expect(refs[0].description).toBeUndefined();
1156
+ });
1157
+ });