@alexkroman1/aai 1.6.0 → 1.7.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.
Files changed (77) hide show
  1. package/.turbo/turbo-build.log +22 -18
  2. package/CHANGELOG.md +16 -0
  3. package/dist/{_internal-types-DFL07G3f.js → _internal-types-CrnTi9Ew.js} +8 -2
  4. package/dist/host/memory-vector.d.ts +17 -0
  5. package/dist/host/pinecone-vector.d.ts +19 -0
  6. package/dist/host/providers/resolve-kv.d.ts +10 -0
  7. package/dist/host/providers/resolve-vector.d.ts +11 -0
  8. package/dist/host/providers/resolve.d.ts +7 -7
  9. package/dist/host/runtime-barrel.d.ts +5 -0
  10. package/dist/host/runtime-barrel.js +416 -149
  11. package/dist/host/runtime.d.ts +6 -0
  12. package/dist/host/server.d.ts +2 -0
  13. package/dist/host/tool-executor.d.ts +2 -0
  14. package/dist/pinecone-CeJ69aRs.js +19 -0
  15. package/dist/{cartesia-BfQPOQ7Y.js → rime-58p9mDR8.js} +19 -19
  16. package/dist/s3-BtCMvCod.js +37 -0
  17. package/dist/sdk/_internal-types.d.ts +11 -1
  18. package/dist/sdk/define.d.ts +5 -1
  19. package/dist/sdk/manifest-barrel.js +1 -1
  20. package/dist/sdk/manifest.d.ts +5 -1
  21. package/dist/sdk/protocol.d.ts +36 -0
  22. package/dist/sdk/protocol.js +26 -1
  23. package/dist/sdk/providers/kv/fs.d.ts +12 -0
  24. package/dist/sdk/providers/kv/memory.d.ts +9 -0
  25. package/dist/sdk/providers/kv/redis.d.ts +17 -0
  26. package/dist/sdk/providers/kv/s3.d.ts +25 -0
  27. package/dist/sdk/providers/kv-barrel.d.ts +13 -0
  28. package/dist/sdk/providers/kv-barrel.js +2 -0
  29. package/dist/sdk/providers/llm-barrel.js +1 -1
  30. package/dist/sdk/providers/stt-barrel.js +1 -1
  31. package/dist/sdk/providers/tts-barrel.js +1 -1
  32. package/dist/sdk/providers/vector/in-memory.d.ts +15 -0
  33. package/dist/sdk/providers/vector/pinecone.d.ts +19 -0
  34. package/dist/sdk/providers/vector-barrel.d.ts +11 -0
  35. package/dist/sdk/providers/vector-barrel.js +2 -0
  36. package/dist/sdk/providers.d.ts +4 -0
  37. package/dist/sdk/types.d.ts +8 -1
  38. package/dist/sdk/vector.d.ts +29 -0
  39. package/dist/{soniox-DCQ3GqJq.js → soniox-BQdL0mB5.js} +13 -13
  40. package/host/_runtime-conformance.ts +21 -0
  41. package/host/_test-utils.ts +1 -0
  42. package/host/memory-vector.test.ts +87 -0
  43. package/host/memory-vector.ts +108 -0
  44. package/host/pinecone-vector.test.ts +79 -0
  45. package/host/pinecone-vector.ts +79 -0
  46. package/host/providers/resolve-kv.test.ts +48 -0
  47. package/host/providers/resolve-kv.ts +128 -0
  48. package/host/providers/resolve-vector.test.ts +26 -0
  49. package/host/providers/resolve-vector.ts +42 -0
  50. package/host/providers/resolve.ts +28 -55
  51. package/host/runtime-barrel.ts +5 -0
  52. package/host/runtime-config.ts +1 -1
  53. package/host/runtime.ts +28 -2
  54. package/host/server.ts +57 -1
  55. package/host/tool-executor.ts +7 -1
  56. package/package.json +20 -29
  57. package/sdk/__snapshots__/exports.test.ts.snap +8 -0
  58. package/sdk/__snapshots__/schema-shapes.test.ts.snap +2 -0
  59. package/sdk/_internal-types.ts +8 -0
  60. package/sdk/define.ts +11 -1
  61. package/sdk/manifest.test.ts +20 -0
  62. package/sdk/manifest.ts +10 -0
  63. package/sdk/protocol-snapshot.test.ts +53 -0
  64. package/sdk/protocol.test.ts +54 -0
  65. package/sdk/protocol.ts +32 -0
  66. package/sdk/providers/kv/fs.ts +20 -0
  67. package/sdk/providers/kv/memory.ts +17 -0
  68. package/sdk/providers/kv/redis.ts +25 -0
  69. package/sdk/providers/kv/s3.ts +33 -0
  70. package/sdk/providers/kv-barrel.ts +19 -0
  71. package/sdk/providers/vector/in-memory.ts +23 -0
  72. package/sdk/providers/vector/pinecone.ts +27 -0
  73. package/sdk/providers/vector-barrel.ts +15 -0
  74. package/sdk/providers.ts +6 -0
  75. package/sdk/types.ts +14 -1
  76. package/sdk/vector.ts +32 -0
  77. /package/dist/{xai-jfQsxxPZ.js → xai-BDI61Y2M.js} +0 -0
@@ -0,0 +1,87 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { _resetMemoryVectorForTests, createMemoryVector } from "./memory-vector.ts";
4
+
5
+ afterEach(() => _resetMemoryVectorForTests());
6
+
7
+ describe("createMemoryVector", () => {
8
+ it("returns empty array when querying an empty namespace", async () => {
9
+ const v = createMemoryVector({ namespace: "ns1" });
10
+ expect(await v.query("anything")).toEqual([]);
11
+ });
12
+
13
+ it("upsert + query returns the upserted record", async () => {
14
+ const v = createMemoryVector({ namespace: "ns1" });
15
+ await v.upsert("doc-1", "the quick brown fox", { tag: "anim" });
16
+ const matches = await v.query("the quick brown fox");
17
+ expect(matches).toHaveLength(1);
18
+ expect(matches[0]).toMatchObject({
19
+ id: "doc-1",
20
+ text: "the quick brown fox",
21
+ metadata: { tag: "anim" },
22
+ });
23
+ expect(matches[0]?.score).toBeGreaterThan(0.9);
24
+ });
25
+
26
+ it("ranks more similar text higher than less similar text", async () => {
27
+ const v = createMemoryVector({ namespace: "ns1" });
28
+ await v.upsert("a", "exact match string");
29
+ await v.upsert("b", "completely different content");
30
+ const matches = await v.query("exact match string", { topK: 2 });
31
+ expect(matches[0]?.id).toBe("a");
32
+ });
33
+
34
+ it("topK caps the number of results", async () => {
35
+ const v = createMemoryVector({ namespace: "ns1" });
36
+ for (let i = 0; i < 10; i++) await v.upsert(`id-${i}`, `text ${i}`);
37
+ expect(await v.query("text", { topK: 3 })).toHaveLength(3);
38
+ });
39
+
40
+ it("idempotent: same id overwrites text and metadata", async () => {
41
+ const v = createMemoryVector({ namespace: "ns1" });
42
+ await v.upsert("doc", "v1", { tag: "old" });
43
+ await v.upsert("doc", "v2", { tag: "new" });
44
+ const matches = await v.query("v2");
45
+ expect(matches[0]?.text).toBe("v2");
46
+ expect(matches[0]?.metadata).toEqual({ tag: "new" });
47
+ });
48
+
49
+ it("delete removes by id", async () => {
50
+ const v = createMemoryVector({ namespace: "ns1" });
51
+ await v.upsert("a", "one");
52
+ await v.upsert("b", "two");
53
+ await v.delete("a");
54
+ const ids = (await v.query("one")).map((m) => m.id);
55
+ expect(ids).not.toContain("a");
56
+ });
57
+
58
+ it("delete accepts an array of ids", async () => {
59
+ const v = createMemoryVector({ namespace: "ns1" });
60
+ await v.upsert("a", "one");
61
+ await v.upsert("b", "two");
62
+ await v.delete(["a", "b"]);
63
+ expect(await v.query("anything")).toEqual([]);
64
+ });
65
+
66
+ it("isolates namespaces", async () => {
67
+ const a = createMemoryVector({ namespace: "ns1" });
68
+ const b = createMemoryVector({ namespace: "ns2" });
69
+ await a.upsert("doc", "in ns1");
70
+ expect(await b.query("in ns1")).toEqual([]);
71
+ expect((await a.query("in ns1"))[0]?.id).toBe("doc");
72
+ });
73
+
74
+ it("filter applies top-level exact-match", async () => {
75
+ const v = createMemoryVector({ namespace: "ns1" });
76
+ await v.upsert("a", "one", { kind: "x" });
77
+ await v.upsert("b", "two", { kind: "y" });
78
+ const matches = await v.query("one", { filter: { kind: "x" } });
79
+ expect(matches.map((m) => m.id)).toEqual(["a"]);
80
+ });
81
+
82
+ it("rejects unsupported filter operators", async () => {
83
+ const v = createMemoryVector({ namespace: "ns1" });
84
+ await v.upsert("a", "one");
85
+ await expect(v.query("one", { filter: { kind: { $in: ["x"] } } })).rejects.toThrow(/operator/i);
86
+ });
87
+ });
@@ -0,0 +1,108 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * In-memory Vector implementation.
4
+ *
5
+ * INTENTIONALLY BAD QUALITY. Pseudo-embedding hashes the text into a
6
+ * 64-dim Float32Array of values in [-1, ~0.99], then L2-normalizes
7
+ * the result. Because both stored and probe vectors are unit-length,
8
+ * cosine similarity reduces to a plain dot product — that's what
9
+ * `cosine()` computes. Used only for `aai dev` and tests — the goal
10
+ * is proving tool wiring, not retrieval ranking.
11
+ */
12
+
13
+ import { createHash } from "node:crypto";
14
+ import type { Vector, VectorMatch, VectorQueryOptions } from "../sdk/vector.ts";
15
+
16
+ export type MemoryVectorOptions = {
17
+ namespace: string;
18
+ };
19
+
20
+ type Record_ = {
21
+ text: string;
22
+ metadata: Record<string, unknown> | undefined;
23
+ vec: Float32Array;
24
+ };
25
+
26
+ const stores = new Map<string, Map<string, Record_>>();
27
+
28
+ function getStore(ns: string): Map<string, Record_> {
29
+ let store = stores.get(ns);
30
+ if (!store) {
31
+ store = new Map();
32
+ stores.set(ns, store);
33
+ }
34
+ return store;
35
+ }
36
+
37
+ const DIM = 64;
38
+
39
+ function pseudoEmbed(text: string): Float32Array {
40
+ const out = new Float32Array(DIM);
41
+ const h1 = createHash("sha256").update(text).digest();
42
+ const h2 = createHash("sha256").update(h1).digest();
43
+ for (let i = 0; i < 32; i++) out[i] = ((h1[i] as number) - 128) / 128;
44
+ for (let i = 0; i < 32; i++) out[i + 32] = ((h2[i] as number) - 128) / 128;
45
+ let norm = 0;
46
+ for (let i = 0; i < DIM; i++) norm += (out[i] as number) * (out[i] as number);
47
+ norm = Math.sqrt(norm) || 1;
48
+ for (let i = 0; i < DIM; i++) out[i] = (out[i] as number) / norm;
49
+ return out;
50
+ }
51
+
52
+ function cosine(a: Float32Array, b: Float32Array): number {
53
+ let dot = 0;
54
+ for (let i = 0; i < DIM; i++) dot += (a[i] as number) * (b[i] as number);
55
+ return dot;
56
+ }
57
+
58
+ function matches(
59
+ metadata: Record<string, unknown> | undefined,
60
+ filter: Record<string, unknown>,
61
+ ): boolean {
62
+ for (const [key, want] of Object.entries(filter)) {
63
+ if (want !== null && typeof want === "object") {
64
+ throw new Error(
65
+ `In-memory Vector: filter operator unsupported (${key}). Only top-level exact-match is supported.`,
66
+ );
67
+ }
68
+ if (metadata?.[key] !== want) return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ export function createMemoryVector(opts: MemoryVectorOptions): Vector {
74
+ const ns = opts.namespace;
75
+
76
+ return {
77
+ async upsert(id, text, metadata) {
78
+ getStore(ns).set(id, { text, metadata, vec: pseudoEmbed(text) });
79
+ },
80
+ async query(text, queryOpts?: VectorQueryOptions) {
81
+ const topK = queryOpts?.topK ?? 5;
82
+ const filter = queryOpts?.filter;
83
+ const probe = pseudoEmbed(text);
84
+ const scored: VectorMatch[] = [];
85
+ for (const [id, rec] of getStore(ns)) {
86
+ if (filter && !matches(rec.metadata, filter)) continue;
87
+ scored.push({
88
+ id,
89
+ score: cosine(probe, rec.vec),
90
+ text: rec.text,
91
+ ...(rec.metadata !== undefined ? { metadata: rec.metadata } : {}),
92
+ });
93
+ }
94
+ scored.sort((a, b) => b.score - a.score);
95
+ return scored.slice(0, topK);
96
+ },
97
+ async delete(ids) {
98
+ const store = getStore(ns);
99
+ const list = Array.isArray(ids) ? ids : [ids];
100
+ for (const id of list) store.delete(id);
101
+ },
102
+ };
103
+ }
104
+
105
+ /** Test-only: clear all in-memory state. Not exported from the package. */
106
+ export function _resetMemoryVectorForTests(): void {
107
+ stores.clear();
108
+ }
@@ -0,0 +1,79 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ // Intercept createRequire from node:module so that require("@pinecone-database/pinecone")
5
+ // returns a controlled fake — works even when the package is not installed.
6
+ vi.mock("node:module", async (importOriginal) => {
7
+ const original = await importOriginal<typeof import("node:module")>();
8
+ return {
9
+ ...original,
10
+ createRequire:
11
+ () =>
12
+ (id: string): unknown => {
13
+ if (id === "@pinecone-database/pinecone") {
14
+ return { Pinecone: PineconeFake };
15
+ }
16
+ return original.createRequire(import.meta.url)(id);
17
+ },
18
+ };
19
+ });
20
+
21
+ // Must use var (not const/let) to avoid TDZ when the vi.mock factory above
22
+ // is hoisted — the variables are referenced inside the factory closure.
23
+ var upsertRecords = vi.fn(); // eslint-disable-line no-var
24
+ var searchRecords = vi.fn(); // eslint-disable-line no-var
25
+ var deleteMany = vi.fn(); // eslint-disable-line no-var
26
+ var namespace = vi.fn(() => ({ upsertRecords, searchRecords, deleteMany })); // eslint-disable-line no-var
27
+ var index = vi.fn(() => ({ namespace })); // eslint-disable-line no-var
28
+ var PineconeFake = vi.fn(function (this: unknown) {
29
+ // eslint-disable-line no-var
30
+ return { index };
31
+ });
32
+
33
+ import { createPineconeVector } from "./pinecone-vector.ts";
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ PineconeFake.mockImplementation(function (this: unknown) {
38
+ return { index };
39
+ });
40
+ index.mockImplementation(() => ({ namespace }));
41
+ namespace.mockImplementation(() => ({ upsertRecords, searchRecords, deleteMany }));
42
+ });
43
+
44
+ describe("createPineconeVector", () => {
45
+ it("threads namespace and index on upsert", async () => {
46
+ const v = createPineconeVector({ apiKey: "k", index: "ix", namespace: "ns" });
47
+ await v.upsert("doc-1", "hello", { tag: "x" });
48
+ expect(index).toHaveBeenCalledWith("ix");
49
+ expect(namespace).toHaveBeenCalledWith("ns");
50
+ expect(upsertRecords).toHaveBeenCalledWith([{ _id: "doc-1", text: "hello", tag: "x" }]);
51
+ });
52
+
53
+ it("calls searchRecords on query", async () => {
54
+ searchRecords.mockResolvedValueOnce({
55
+ result: {
56
+ hits: [{ _id: "doc-1", _score: 0.9, fields: { text: "hello", tag: "x" } }],
57
+ },
58
+ });
59
+ const v = createPineconeVector({ apiKey: "k", index: "ix", namespace: "ns" });
60
+ const matches = await v.query("hello", { topK: 3, filter: { tag: "x" } });
61
+ expect(searchRecords).toHaveBeenCalledWith({
62
+ query: { inputs: { text: "hello" }, topK: 3, filter: { tag: "x" } },
63
+ fields: ["*"],
64
+ });
65
+ expect(matches).toEqual([{ id: "doc-1", score: 0.9, text: "hello", metadata: { tag: "x" } }]);
66
+ });
67
+
68
+ it("delete with single id", async () => {
69
+ const v = createPineconeVector({ apiKey: "k", index: "ix", namespace: "ns" });
70
+ await v.delete("doc-1");
71
+ expect(deleteMany).toHaveBeenCalledWith(["doc-1"]);
72
+ });
73
+
74
+ it("delete with array", async () => {
75
+ const v = createPineconeVector({ apiKey: "k", index: "ix", namespace: "ns" });
76
+ await v.delete(["a", "b"]);
77
+ expect(deleteMany).toHaveBeenCalledWith(["a", "b"]);
78
+ });
79
+ });
@@ -0,0 +1,79 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Pinecone Vector implementation using integrated inference.
4
+ *
5
+ * The Pinecone index must be created with an embed config (e.g.
6
+ * `multilingual-e5-large`); we never see the embedding vectors.
7
+ *
8
+ * `@pinecone-database/pinecone` is loaded lazily via `createRequire`
9
+ * so the package becomes a true *optional* peer dependency: if a
10
+ * deployment doesn't use Pinecone, the missing package is never
11
+ * imported.
12
+ */
13
+
14
+ import type { Vector, VectorMatch, VectorQueryOptions } from "../sdk/vector.ts";
15
+ import { loadProviderPackage } from "./providers/resolve.ts";
16
+
17
+ export type PineconeVectorOptions = {
18
+ apiKey: string;
19
+ index: string;
20
+ /** Per-tenant namespace — set by the server to the agent slug. */
21
+ namespace: string;
22
+ };
23
+
24
+ type PineconeNs = {
25
+ upsertRecords: (records: Record<string, unknown>[]) => Promise<unknown>;
26
+ searchRecords: (req: {
27
+ query: { inputs: { text: string }; topK: number; filter?: Record<string, unknown> };
28
+ fields: string[];
29
+ }) => Promise<{
30
+ result: { hits: Array<{ _id: string; _score: number; fields: Record<string, unknown> }> };
31
+ }>;
32
+ deleteMany: (ids: string[]) => Promise<unknown>;
33
+ };
34
+
35
+ type PineconeClient = {
36
+ index: (name: string) => { namespace: (ns: string) => PineconeNs };
37
+ };
38
+
39
+ export function createPineconeVector(opts: PineconeVectorOptions): Vector {
40
+ // Lazy-load via loadProviderPackage so the package is a true optional peer dep.
41
+ const { Pinecone } = loadProviderPackage<{
42
+ Pinecone: new (opts: { apiKey: string }) => PineconeClient;
43
+ }>("@pinecone-database/pinecone", "Pinecone Vector");
44
+ const client = new Pinecone({ apiKey: opts.apiKey });
45
+ const ns = (): PineconeNs => client.index(opts.index).namespace(opts.namespace);
46
+
47
+ return {
48
+ async upsert(id, text, metadata) {
49
+ const record: Record<string, unknown> = { _id: id, text, ...(metadata ?? {}) };
50
+ await ns().upsertRecords([record]);
51
+ },
52
+ async query(text, queryOpts?: VectorQueryOptions) {
53
+ const topK = queryOpts?.topK ?? 5;
54
+ const req = {
55
+ query: {
56
+ inputs: { text },
57
+ topK,
58
+ ...(queryOpts?.filter !== undefined ? { filter: queryOpts.filter } : {}),
59
+ },
60
+ fields: ["*"],
61
+ };
62
+ const resp = await ns().searchRecords(req);
63
+ return resp.result.hits.map((hit): VectorMatch => {
64
+ const { text: hitText, ...rest } = hit.fields;
65
+ const metadata = Object.keys(rest).length > 0 ? rest : undefined;
66
+ return {
67
+ id: hit._id,
68
+ score: hit._score,
69
+ text: typeof hitText === "string" ? hitText : "",
70
+ ...(metadata !== undefined ? { metadata } : {}),
71
+ };
72
+ });
73
+ },
74
+ async delete(ids) {
75
+ const list = Array.isArray(ids) ? ids : [ids];
76
+ await ns().deleteMany(list);
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,48 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import { describe, expect, it } from "vitest";
3
+ import { fsKv } from "../../sdk/providers/kv/fs.ts";
4
+ import { memoryKv } from "../../sdk/providers/kv/memory.ts";
5
+ import { redisKv } from "../../sdk/providers/kv/redis.ts";
6
+ import { s3Kv } from "../../sdk/providers/kv/s3.ts";
7
+ import { resolveKv } from "./resolve-kv.ts";
8
+
9
+ describe("resolveKv", () => {
10
+ it("resolves memoryKv to a working in-memory KV", async () => {
11
+ const kv = resolveKv(memoryKv(), {}, "p");
12
+ await kv.set("k", "v");
13
+ expect(await kv.get("k")).toBe("v");
14
+ });
15
+
16
+ it("resolves fsKv with explicit base", () => {
17
+ expect(() => resolveKv(fsKv({ base: "/tmp/aai-test" }), {}, "p")).not.toThrow();
18
+ });
19
+
20
+ it("resolves s3Kv when AWS creds are present", () => {
21
+ expect(() =>
22
+ resolveKv(
23
+ s3Kv({ bucket: "b", endpoint: "https://e", region: "auto" }),
24
+ {
25
+ AWS_ACCESS_KEY_ID: "k",
26
+ AWS_SECRET_ACCESS_KEY: "s",
27
+ },
28
+ "p",
29
+ ),
30
+ ).not.toThrow();
31
+ });
32
+
33
+ it("throws when s3Kv has no AWS creds", () => {
34
+ expect(() => resolveKv(s3Kv({ bucket: "b" }), {}, "p")).toThrow(/AWS_ACCESS_KEY_ID/);
35
+ });
36
+
37
+ it("resolves redisKv when REDIS_URL is present", () => {
38
+ expect(() => resolveKv(redisKv(), { REDIS_URL: "redis://localhost:6379" }, "p")).not.toThrow();
39
+ });
40
+
41
+ it("throws when redisKv has no REDIS_URL", () => {
42
+ expect(() => resolveKv(redisKv(), {}, "p")).toThrow(/REDIS_URL/);
43
+ });
44
+
45
+ it("throws on unknown kind", () => {
46
+ expect(() => resolveKv({ kind: "nope", options: {} }, {}, "p")).toThrow(/Unknown KV provider/);
47
+ });
48
+ });
@@ -0,0 +1,128 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Descriptor → concrete `Kv` resolver. Mirror of `resolveLlm` /
4
+ * `resolveVector`. Always wraps the produced unstorage Storage in
5
+ * `createUnstorageKv` with the provided per-tenant prefix so namespace
6
+ * isolation is enforced regardless of backend choice.
7
+ */
8
+
9
+ import { createStorage, type Driver } from "unstorage";
10
+ import type { Kv } from "../../sdk/kv.ts";
11
+ import { FS_KV_KIND, type FsKvOptions } from "../../sdk/providers/kv/fs.ts";
12
+ import { MEMORY_KV_KIND } from "../../sdk/providers/kv/memory.ts";
13
+ import { REDIS_KV_KIND, type RedisKvOptions } from "../../sdk/providers/kv/redis.ts";
14
+ import { S3_KV_KIND, type S3KvOptions } from "../../sdk/providers/kv/s3.ts";
15
+ import type { KvProvider } from "../../sdk/providers.ts";
16
+ import { createUnstorageKv } from "../unstorage-kv.ts";
17
+ import { loadProviderPackage, resolveApiKey } from "./resolve.ts";
18
+
19
+ /**
20
+ * Load a CJS unstorage driver factory. The CJS variants use
21
+ * `module.exports = defineDriver(...)` so the require result is the
22
+ * factory itself (not an object with `.default`).
23
+ *
24
+ * Delegates to loadProviderPackage (lazy-load via createRequire so the
25
+ * driver is a true optional peer dep).
26
+ */
27
+ function loadDriver<T>(modulePath: string, label: string): T {
28
+ return loadProviderPackage<T>(modulePath, `${label} KV: driver`);
29
+ }
30
+
31
+ /**
32
+ * Build a lazy unstorage Driver that defers loading the real driver
33
+ * factory until the first I/O operation. This is necessary for drivers
34
+ * whose peer dependencies (e.g. `ioredis`) may not be installed on the
35
+ * host at startup — the missing package will only surface when the agent
36
+ * actually performs KV operations, not at session creation time.
37
+ */
38
+ function makeLazyDriver(modulePath: string, label: string, opts: Record<string, unknown>): Driver {
39
+ let resolved: Driver | null = null;
40
+ const get = (): Driver => {
41
+ if (!resolved) {
42
+ const factory = loadDriver<(o: Record<string, unknown>) => Driver>(modulePath, label);
43
+ resolved = factory(opts);
44
+ }
45
+ return resolved;
46
+ };
47
+ return {
48
+ name: label.toLowerCase(),
49
+ hasItem: (key, txOpts) => get().hasItem(key, txOpts),
50
+ getItem: (key, txOpts) => get().getItem(key, txOpts),
51
+ getItemRaw: (key, txOpts) => get().getItemRaw?.(key, txOpts) ?? null,
52
+ setItem: (key, value, txOpts) => get().setItem?.(key, value, txOpts),
53
+ setItemRaw: (key, value, txOpts) => get().setItemRaw?.(key, value, txOpts),
54
+ removeItem: (key, txOpts) => get().removeItem?.(key, txOpts),
55
+ getKeys: (base, txOpts) => get().getKeys(base, txOpts),
56
+ clear: (base, txOpts) => get().clear?.(base, txOpts),
57
+ dispose: () => (resolved ? resolved.dispose?.() : undefined),
58
+ };
59
+ }
60
+
61
+ /** Resolve a {@link KvProvider} descriptor into a {@link Kv}. */
62
+ export function resolveKv(descriptor: KvProvider, env: Record<string, string>, prefix: string): Kv {
63
+ switch (descriptor.kind) {
64
+ case MEMORY_KV_KIND: {
65
+ return createUnstorageKv({ storage: createStorage(), prefix });
66
+ }
67
+ case FS_KV_KIND: {
68
+ const opts = descriptor.options as unknown as FsKvOptions;
69
+ const fsDriver = loadDriver<(o: { base: string }) => Driver>("unstorage/drivers/fs", "fs");
70
+ return createUnstorageKv({
71
+ storage: createStorage({ driver: fsDriver({ base: opts.base }) }),
72
+ prefix,
73
+ });
74
+ }
75
+ case S3_KV_KIND: {
76
+ const opts = descriptor.options as unknown as S3KvOptions;
77
+ const accessKeyId = resolveApiKey("AWS_ACCESS_KEY_ID", env);
78
+ const secretAccessKey = resolveApiKey("AWS_SECRET_ACCESS_KEY", env);
79
+ if (!(accessKeyId && secretAccessKey)) {
80
+ throw new Error(
81
+ "S3 KV: missing AWS credentials. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the agent env.",
82
+ );
83
+ }
84
+ const s3Driver = loadDriver<
85
+ (o: {
86
+ bucket: string;
87
+ endpoint?: string;
88
+ region: string;
89
+ accessKeyId: string;
90
+ secretAccessKey: string;
91
+ }) => Driver
92
+ >("unstorage/drivers/s3", "S3");
93
+ return createUnstorageKv({
94
+ storage: createStorage({
95
+ driver: s3Driver({
96
+ bucket: opts.bucket,
97
+ ...(opts.endpoint !== undefined ? { endpoint: opts.endpoint } : {}),
98
+ region: opts.region ?? "auto",
99
+ accessKeyId,
100
+ secretAccessKey,
101
+ }),
102
+ }),
103
+ prefix,
104
+ });
105
+ }
106
+ case REDIS_KV_KIND: {
107
+ const opts = descriptor.options as unknown as RedisKvOptions;
108
+ const url = resolveApiKey("REDIS_URL", env);
109
+ if (!url) {
110
+ throw new Error("Redis KV: missing connection URL. Set REDIS_URL in the agent env.");
111
+ }
112
+ return createUnstorageKv({
113
+ storage: createStorage({
114
+ driver: makeLazyDriver("unstorage/drivers/redis", "Redis", {
115
+ url,
116
+ ...(opts.tls !== undefined ? { tls: opts.tls } : {}),
117
+ }),
118
+ }),
119
+ prefix,
120
+ });
121
+ }
122
+ default:
123
+ throw new Error(
124
+ `Unknown KV provider kind: "${descriptor.kind}". ` +
125
+ `Supported: ${MEMORY_KV_KIND}, ${FS_KV_KIND}, ${S3_KV_KIND}, ${REDIS_KV_KIND}.`,
126
+ );
127
+ }
128
+ }
@@ -0,0 +1,26 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { inMemoryVector } from "../../sdk/providers/vector/in-memory.ts";
4
+ import { pinecone } from "../../sdk/providers/vector/pinecone.ts";
5
+ import { _resetMemoryVectorForTests } from "../memory-vector.ts";
6
+ import { resolveVector } from "./resolve-vector.ts";
7
+
8
+ afterEach(() => _resetMemoryVectorForTests());
9
+
10
+ describe("resolveVector", () => {
11
+ it("resolves inMemoryVector descriptor to a working store", async () => {
12
+ const v = resolveVector(inMemoryVector(), {}, "ns1");
13
+ await v.upsert("doc", "hello");
14
+ expect((await v.query("hello"))[0]?.id).toBe("doc");
15
+ });
16
+
17
+ it("throws clear error when PINECONE_API_KEY is missing", () => {
18
+ expect(() => resolveVector(pinecone({ index: "ix" }), {}, "ns")).toThrow(/PINECONE_API_KEY/);
19
+ });
20
+
21
+ it("throws on unknown kind", () => {
22
+ expect(() => resolveVector({ kind: "nope", options: {} }, {}, "ns")).toThrow(
23
+ /Unknown Vector provider/,
24
+ );
25
+ });
26
+ });
@@ -0,0 +1,42 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Descriptor → concrete `Vector` resolver. Mirror of `resolveLlm`.
4
+ *
5
+ * Pulls API keys from the agent env so descriptors stay
6
+ * secret-free. Lazy-loads provider SDKs via `createRequire` so
7
+ * unused providers never enter the bundle.
8
+ */
9
+
10
+ import { IN_MEMORY_VECTOR_KIND } from "../../sdk/providers/vector/in-memory.ts";
11
+ import { PINECONE_VECTOR_KIND, type PineconeOptions } from "../../sdk/providers/vector/pinecone.ts";
12
+ import type { VectorProvider } from "../../sdk/providers.ts";
13
+ import type { Vector } from "../../sdk/vector.ts";
14
+ import { createMemoryVector } from "../memory-vector.ts";
15
+ import { createPineconeVector } from "../pinecone-vector.ts";
16
+ import { resolveApiKey } from "./resolve.ts";
17
+
18
+ /** Resolve a {@link VectorProvider} descriptor into a {@link Vector}. */
19
+ export function resolveVector(
20
+ descriptor: VectorProvider,
21
+ env: Record<string, string>,
22
+ namespace: string,
23
+ ): Vector {
24
+ switch (descriptor.kind) {
25
+ case IN_MEMORY_VECTOR_KIND: {
26
+ return createMemoryVector({ namespace });
27
+ }
28
+ case PINECONE_VECTOR_KIND: {
29
+ const apiKey = resolveApiKey("PINECONE_API_KEY", env);
30
+ if (!apiKey) {
31
+ throw new Error("Pinecone Vector: missing API key. Set PINECONE_API_KEY in the agent env.");
32
+ }
33
+ const opts = descriptor.options as unknown as PineconeOptions;
34
+ return createPineconeVector({ apiKey, index: opts.index, namespace });
35
+ }
36
+ default:
37
+ throw new Error(
38
+ `Unknown Vector provider kind: "${descriptor.kind}". ` +
39
+ `Supported: ${IN_MEMORY_VECTOR_KIND}, ${PINECONE_VECTOR_KIND}.`,
40
+ );
41
+ }
42
+ }