nexus-ai-pro 2.1.0 → 2.2.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 (194) hide show
  1. package/API_STABILITY.md +38 -0
  2. package/CHANGELOG.md +145 -1
  3. package/README.md +13 -8
  4. package/dist/cache/memory-cache.d.ts +8 -0
  5. package/dist/cache/memory-cache.js +9 -0
  6. package/dist/cache/semantic-cache.d.ts +6 -3
  7. package/dist/cache/semantic-cache.js +5 -2
  8. package/dist/cli/args.js +3 -0
  9. package/dist/cli/db.d.ts +16 -4
  10. package/dist/cli/db.js +169 -22
  11. package/dist/cli/doctor.d.ts +17 -0
  12. package/dist/cli/doctor.js +53 -0
  13. package/dist/cli.js +12 -1
  14. package/dist/core/lifecycle.js +11 -6
  15. package/dist/core/nexus.d.ts +6 -0
  16. package/dist/core/nexus.js +103 -22
  17. package/dist/doctor/index.d.ts +90 -0
  18. package/dist/doctor/index.js +237 -0
  19. package/dist/graph/functional.d.ts +80 -7
  20. package/dist/graph/functional.js +138 -35
  21. package/dist/graph/graph.d.ts +15 -5
  22. package/dist/graph/graph.js +72 -20
  23. package/dist/images/evals.js +4 -2
  24. package/dist/operations/adapters.d.ts +52 -4
  25. package/dist/operations/adapters.js +194 -7
  26. package/dist/operations/runner.js +1 -0
  27. package/dist/operations/stats.js +8 -1
  28. package/dist/operations/store.d.ts +5 -1
  29. package/dist/operations/store.js +7 -0
  30. package/dist/ops/circuit-breaker.d.ts +52 -0
  31. package/dist/ops/circuit-breaker.js +4 -0
  32. package/dist/ops/circuit-store.d.ts +13 -2
  33. package/dist/ops/circuit-store.js +68 -0
  34. package/dist/ops/circuit-sync.d.ts +17 -0
  35. package/dist/ops/circuit-sync.js +86 -0
  36. package/dist/ops/rate-limiter.d.ts +2 -0
  37. package/dist/ops/rate-limiter.js +2 -0
  38. package/dist/postgres/circuits.d.ts +21 -4
  39. package/dist/postgres/circuits.js +65 -4
  40. package/dist/postgres/client.d.ts +0 -2
  41. package/dist/postgres/client.js +0 -4
  42. package/dist/postgres/evaluate.d.ts +16 -4
  43. package/dist/postgres/evaluate.js +17 -5
  44. package/dist/postgres/index.d.ts +32 -11
  45. package/dist/postgres/index.js +65 -31
  46. package/dist/postgres/migrations.d.ts +60 -0
  47. package/dist/postgres/migrations.js +121 -0
  48. package/dist/postgres/operations.d.ts +14 -3
  49. package/dist/postgres/operations.js +45 -15
  50. package/dist/postgres/prompts.d.ts +11 -2
  51. package/dist/postgres/prompts.js +14 -3
  52. package/dist/postgres/rollups.d.ts +35 -0
  53. package/dist/postgres/rollups.js +129 -0
  54. package/dist/postgres/store.d.ts +11 -2
  55. package/dist/postgres/store.js +14 -3
  56. package/dist/postgres/traces.d.ts +13 -3
  57. package/dist/postgres/traces.js +25 -3
  58. package/dist/postgres/vectors.d.ts +11 -2
  59. package/dist/postgres/vectors.js +14 -3
  60. package/dist/router/auto-router.d.ts +34 -7
  61. package/dist/router/auto-router.js +201 -68
  62. package/dist/server/assistant.d.ts +4 -0
  63. package/dist/server/assistant.js +8 -0
  64. package/dist/server/errors.d.ts +12 -0
  65. package/dist/server/errors.js +7 -0
  66. package/dist/server/index.d.ts +1 -1
  67. package/dist/server/index.js +1 -1
  68. package/dist/server/runs.d.ts +24 -1
  69. package/dist/server/runs.js +90 -14
  70. package/dist/sqlite/index.d.ts +16 -3
  71. package/dist/sqlite/index.js +16 -3
  72. package/dist/sqlite/migrations.d.ts +26 -0
  73. package/dist/sqlite/migrations.js +63 -0
  74. package/dist/sqlite/operations.d.ts +14 -3
  75. package/dist/sqlite/operations.js +42 -15
  76. package/dist/sqlite/store.d.ts +9 -2
  77. package/dist/sqlite/store.js +13 -3
  78. package/dist/sqlite/vectors.d.ts +11 -2
  79. package/dist/sqlite/vectors.js +13 -2
  80. package/dist/store/tenant.d.ts +12 -0
  81. package/dist/store/tenant.js +29 -0
  82. package/dist/tenancy/index.d.ts +113 -0
  83. package/dist/tenancy/index.js +233 -0
  84. package/dist/tracing/instrument.js +2 -0
  85. package/dist/types/config.d.ts +24 -2
  86. package/dist/types/graph.d.ts +11 -1
  87. package/dist/types/messages.d.ts +5 -0
  88. package/dist/types/operations.d.ts +6 -0
  89. package/dist/types/response.d.ts +20 -0
  90. package/dist/types/server.d.ts +10 -1
  91. package/dist/utils/schema-migrations.d.ts +97 -0
  92. package/dist/utils/schema-migrations.js +91 -0
  93. package/dist/utils/signals.d.ts +17 -0
  94. package/dist/utils/signals.js +26 -0
  95. package/dist/utils/tenant.d.ts +14 -0
  96. package/dist/utils/tenant.js +16 -0
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist-cjs/cache/memory-cache.d.ts +8 -0
  100. package/dist-cjs/cache/memory-cache.js +9 -0
  101. package/dist-cjs/cache/semantic-cache.d.ts +6 -3
  102. package/dist-cjs/cache/semantic-cache.js +5 -2
  103. package/dist-cjs/cli/args.js +3 -0
  104. package/dist-cjs/cli/db.d.ts +16 -4
  105. package/dist-cjs/cli/db.js +200 -20
  106. package/dist-cjs/cli/doctor.d.ts +17 -0
  107. package/dist-cjs/cli/doctor.js +56 -0
  108. package/dist-cjs/core/lifecycle.js +11 -6
  109. package/dist-cjs/core/nexus.d.ts +6 -0
  110. package/dist-cjs/core/nexus.js +103 -22
  111. package/dist-cjs/doctor/index.d.ts +90 -0
  112. package/dist-cjs/doctor/index.js +276 -0
  113. package/dist-cjs/graph/functional.d.ts +80 -7
  114. package/dist-cjs/graph/functional.js +137 -34
  115. package/dist-cjs/graph/graph.d.ts +15 -5
  116. package/dist-cjs/graph/graph.js +72 -20
  117. package/dist-cjs/images/evals.js +4 -2
  118. package/dist-cjs/operations/adapters.d.ts +52 -4
  119. package/dist-cjs/operations/adapters.js +193 -6
  120. package/dist-cjs/operations/runner.js +1 -0
  121. package/dist-cjs/operations/stats.js +8 -1
  122. package/dist-cjs/operations/store.d.ts +5 -1
  123. package/dist-cjs/operations/store.js +7 -0
  124. package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
  125. package/dist-cjs/ops/circuit-breaker.js +4 -0
  126. package/dist-cjs/ops/circuit-store.d.ts +13 -2
  127. package/dist-cjs/ops/circuit-store.js +68 -0
  128. package/dist-cjs/ops/circuit-sync.d.ts +17 -0
  129. package/dist-cjs/ops/circuit-sync.js +87 -0
  130. package/dist-cjs/ops/rate-limiter.d.ts +2 -0
  131. package/dist-cjs/ops/rate-limiter.js +2 -0
  132. package/dist-cjs/postgres/circuits.d.ts +21 -4
  133. package/dist-cjs/postgres/circuits.js +98 -3
  134. package/dist-cjs/postgres/client.d.ts +0 -2
  135. package/dist-cjs/postgres/client.js +0 -5
  136. package/dist-cjs/postgres/evaluate.d.ts +16 -4
  137. package/dist-cjs/postgres/evaluate.js +50 -4
  138. package/dist-cjs/postgres/index.d.ts +32 -11
  139. package/dist-cjs/postgres/index.js +69 -20
  140. package/dist-cjs/postgres/migrations.d.ts +60 -0
  141. package/dist-cjs/postgres/migrations.js +129 -0
  142. package/dist-cjs/postgres/operations.d.ts +14 -3
  143. package/dist-cjs/postgres/operations.js +78 -14
  144. package/dist-cjs/postgres/prompts.d.ts +11 -2
  145. package/dist-cjs/postgres/prompts.js +47 -2
  146. package/dist-cjs/postgres/rollups.d.ts +35 -0
  147. package/dist-cjs/postgres/rollups.js +168 -0
  148. package/dist-cjs/postgres/store.d.ts +11 -2
  149. package/dist-cjs/postgres/store.js +47 -2
  150. package/dist-cjs/postgres/traces.d.ts +13 -3
  151. package/dist-cjs/postgres/traces.js +58 -2
  152. package/dist-cjs/postgres/vectors.d.ts +11 -2
  153. package/dist-cjs/postgres/vectors.js +47 -2
  154. package/dist-cjs/router/auto-router.d.ts +34 -7
  155. package/dist-cjs/router/auto-router.js +201 -68
  156. package/dist-cjs/server/assistant.d.ts +4 -0
  157. package/dist-cjs/server/assistant.js +8 -0
  158. package/dist-cjs/server/errors.d.ts +12 -0
  159. package/dist-cjs/server/errors.js +9 -1
  160. package/dist-cjs/server/index.d.ts +1 -1
  161. package/dist-cjs/server/index.js +2 -1
  162. package/dist-cjs/server/runs.d.ts +24 -1
  163. package/dist-cjs/server/runs.js +89 -13
  164. package/dist-cjs/sqlite/index.d.ts +16 -3
  165. package/dist-cjs/sqlite/index.js +31 -10
  166. package/dist-cjs/sqlite/migrations.d.ts +26 -0
  167. package/dist-cjs/sqlite/migrations.js +71 -0
  168. package/dist-cjs/sqlite/operations.d.ts +14 -3
  169. package/dist-cjs/sqlite/operations.js +76 -15
  170. package/dist-cjs/sqlite/store.d.ts +9 -2
  171. package/dist-cjs/sqlite/store.js +47 -3
  172. package/dist-cjs/sqlite/vectors.d.ts +11 -2
  173. package/dist-cjs/sqlite/vectors.js +47 -2
  174. package/dist-cjs/store/tenant.d.ts +12 -0
  175. package/dist-cjs/store/tenant.js +33 -0
  176. package/dist-cjs/tenancy/index.d.ts +113 -0
  177. package/dist-cjs/tenancy/index.js +250 -0
  178. package/dist-cjs/tracing/instrument.js +2 -0
  179. package/dist-cjs/types/config.d.ts +24 -2
  180. package/dist-cjs/types/graph.d.ts +11 -1
  181. package/dist-cjs/types/messages.d.ts +5 -0
  182. package/dist-cjs/types/operations.d.ts +6 -0
  183. package/dist-cjs/types/response.d.ts +20 -0
  184. package/dist-cjs/types/server.d.ts +10 -1
  185. package/dist-cjs/utils/schema-migrations.d.ts +97 -0
  186. package/dist-cjs/utils/schema-migrations.js +102 -0
  187. package/dist-cjs/utils/signals.d.ts +17 -0
  188. package/dist-cjs/utils/signals.js +29 -0
  189. package/dist-cjs/utils/tenant.d.ts +14 -0
  190. package/dist-cjs/utils/tenant.js +22 -0
  191. package/dist-cjs/version.d.ts +1 -1
  192. package/dist-cjs/version.js +1 -1
  193. package/llms.txt +4 -3
  194. package/package.json +71 -2
@@ -21,6 +21,16 @@ export function sqliteStoreMigration(options = {}) {
21
21
  `CREATE INDEX IF NOT EXISTS "${name}_expiry" ON ${table} (expires_at) WHERE expires_at IS NOT NULL`,
22
22
  ];
23
23
  }
24
+ export function sqliteStoreMigrations(options = {}) {
25
+ return [
26
+ {
27
+ component: `store:${options.table ?? DEFAULT_TABLE}`,
28
+ version: 1,
29
+ name: 'create the store table',
30
+ statements: sqliteStoreMigration(options),
31
+ },
32
+ ];
33
+ }
24
34
  export class SqliteStore {
25
35
  options;
26
36
  client;
@@ -32,9 +42,9 @@ export class SqliteStore {
32
42
  this.table = quoteSqliteTable(options.table ?? DEFAULT_TABLE);
33
43
  this.now = options.now ?? (() => new Date());
34
44
  }
35
- async migrate() {
36
- for (const statement of sqliteStoreMigration(this.options))
37
- await this.client.exec(statement);
45
+ async migrate(options = {}) {
46
+ const { applySqliteMigrations } = await import('./migrations.js');
47
+ return applySqliteMigrations(this.client, sqliteStoreMigrations(this.options), options);
38
48
  }
39
49
  async put(namespace, key, value, options = {}) {
40
50
  if (!key.trim())
@@ -1,5 +1,7 @@
1
1
  import { type EmbeddingProvider, type VectorDocument, type VectorSearchOptions, type VectorSearchResult, type VectorStore } from '../hallucination/retrieval.js';
2
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
2
3
  import { type SqliteDatabaseLike, type SqliteLikeClient } from './client.js';
4
+ import type { SqliteMigrateOptions } from './migrations.js';
3
5
  /** Options for the SQLite vector store. */
4
6
  export interface SqliteVectorStoreOptions {
5
7
  /** Width of every vector. Must match the embedding function's output. */
@@ -20,6 +22,10 @@ export interface SqliteVectorStoreOptions {
20
22
  export declare function sqliteVectorStoreMigration(options?: {
21
23
  table?: string;
22
24
  }): string;
25
+ /** The versioned schema, which `migrate()` applies. Version 1 is the 2.0 table. */
26
+ export declare function sqliteVectorStoreMigrations(options?: {
27
+ table?: string;
28
+ }): SchemaMigration[];
23
29
  /**
24
30
  * Retrieval chunks in SQLite: one file, no server, the same contract as every other store.
25
31
  *
@@ -34,8 +40,11 @@ export declare class SqliteVectorStore implements VectorStore {
34
40
  private readonly table;
35
41
  private readonly embed;
36
42
  constructor(database: SqliteLikeClient | SqliteDatabaseLike, options: SqliteVectorStoreOptions);
37
- /** Creates the table unless it exists. Never runs implicitly. */
38
- migrate(): Promise<void>;
43
+ /**
44
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, each inside
45
+ * `BEGIN IMMEDIATE`. Never runs implicitly. Safe on a file an older release created.
46
+ */
47
+ migrate(options?: SqliteMigrateOptions): Promise<MigrationResult>;
39
48
  /** Adds chunks, or replaces those whose id exists, embedding those without a vector in one batch. */
40
49
  add(documents: VectorDocument[]): Promise<void>;
41
50
  /** The chunks most similar to a query, best first. */
@@ -5,6 +5,16 @@ export function sqliteVectorStoreMigration(options = {}) {
5
5
  const table = quoteSqliteTable(options.table ?? 'nexus_vectors');
6
6
  return `CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, content TEXT NOT NULL, source TEXT, metadata TEXT, embedding BLOB NOT NULL)`;
7
7
  }
8
+ export function sqliteVectorStoreMigrations(options = {}) {
9
+ return [
10
+ {
11
+ component: `vectors:${options.table ?? 'nexus_vectors'}`,
12
+ version: 1,
13
+ name: 'create the vectors table',
14
+ statements: [sqliteVectorStoreMigration(options)],
15
+ },
16
+ ];
17
+ }
8
18
  export class SqliteVectorStore {
9
19
  options;
10
20
  client;
@@ -17,8 +27,9 @@ export class SqliteVectorStore {
17
27
  this.table = quoteSqliteTable(options.table ?? 'nexus_vectors');
18
28
  this.embed = options.embed ?? ((texts) => createHashEmbeddings(texts, options.dimensions));
19
29
  }
20
- async migrate() {
21
- await this.client.exec(sqliteVectorStoreMigration(this.options));
30
+ async migrate(options = {}) {
31
+ const { applySqliteMigrations } = await import('./migrations.js');
32
+ return applySqliteMigrations(this.client, sqliteVectorStoreMigrations(this.options), options);
22
33
  }
23
34
  async add(documents) {
24
35
  if (documents.length === 0)
@@ -0,0 +1,12 @@
1
+ import type { Store } from '../types/store.js';
2
+ /** The first namespace part under which every tenant's items live, so they never meet an application's own. */
3
+ export declare const TENANT_NAMESPACE = "nexus:tenant";
4
+ /**
5
+ * One tenant's view of a long-term store: every namespace it reads or writes is placed under
6
+ * `['nexus:tenant', tenantId]`, and every item it returns has that prefix taken off again. A search
7
+ * or a namespace listing cannot reach past it, so another tenant's items are not merely filtered out
8
+ * but never read. Works over any store — memory, Redis, Postgres, SQLite — with no schema change.
9
+ *
10
+ * A graph run given a `tenantId` sees its store through this view as `context.store`.
11
+ */
12
+ export declare function tenantStore(store: Store, tenantId: string): Store;
@@ -0,0 +1,29 @@
1
+ import { assertTenantId } from '../utils/tenant.js';
2
+ export const TENANT_NAMESPACE = 'nexus:tenant';
3
+ export function tenantStore(store, tenantId) {
4
+ const root = [TENANT_NAMESPACE, assertTenantId(tenantId)];
5
+ const scope = (namespace) => [...root, ...namespace];
6
+ const unscope = (item) => ({ ...item, namespace: item.namespace.slice(root.length) });
7
+ return {
8
+ put(namespace, key, value, options) {
9
+ return store.put(scope(namespace), key, value, options);
10
+ },
11
+ async get(namespace, key) {
12
+ const item = await store.get(scope(namespace), key);
13
+ return item ? unscope(item) : undefined;
14
+ },
15
+ delete(namespace, key) {
16
+ return store.delete(scope(namespace), key);
17
+ },
18
+ async search(namespacePrefix, options) {
19
+ return (await store.search(scope(namespacePrefix), options)).map(unscope);
20
+ },
21
+ async listNamespaces(options = {}) {
22
+ const namespaces = await store.listNamespaces({
23
+ prefix: scope(options.prefix ?? []),
24
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
25
+ });
26
+ return namespaces.map((namespace) => namespace.slice(root.length)).filter((namespace) => namespace.length > 0);
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * One tenant model for every store.
3
+ *
4
+ * Each function returns one tenant's view of a shared store, implementing the same contract as the
5
+ * store itself, so a tenant's code takes a `TraceStore`, a `PromptStore`, or a `VectorStore` as it
6
+ * always did and cannot reach past its tenant. Names and keys are placed under the tenant's prefix
7
+ * (`tenant/<id>/`) and records carry `tenantId`, so the views work over the memory, Redis, Postgres,
8
+ * SQLite, and file stores alike, with no schema change. A tenant id is letters, digits, `_`, and `-`,
9
+ * so no tenant's prefix is the start of another's.
10
+ */
11
+ import type { CacheAdapter } from '../cache/adapters.js';
12
+ import type { VectorStore } from '../hallucination/retrieval.js';
13
+ import type { AssetPutOptions, AssetSignOptions, AssetStore } from '../images/asset-support.js';
14
+ import type { RollupStore } from '../tracing/rollups.js';
15
+ import type { DatasetStore, ExperimentStore } from '../types/evaluate.js';
16
+ import type { OperationStore } from '../types/operations.js';
17
+ import type { PromptStore } from '../types/prompts.js';
18
+ import type { Store } from '../types/store.js';
19
+ import type { TraceStore } from '../types/tracing.js';
20
+ export { TENANT_NAMESPACE, tenantStore } from '../store/tenant.js';
21
+ export { assertTenantId, TenantIdError } from '../utils/tenant.js';
22
+ /**
23
+ * One tenant's traces. Every run saved through it is stamped `metadata.tenantId`, the field rollups,
24
+ * the studio, and the Postgres tenant index read; `get()`, `query()`, and `tree()` return only runs so
25
+ * stamped, and feedback reaches only the tenant's own runs. `prune()` is left out, since it would
26
+ * remove every tenant's old runs.
27
+ */
28
+ export declare function tenantTraceStore(store: TraceStore, tenantId: string): TraceStore;
29
+ /**
30
+ * One tenant's operations. Records it creates carry `tenantId`, and their idempotency keys are
31
+ * placed under the tenant's prefix, so two tenants using the same key never collide. Reads, updates,
32
+ * deletes, recovery, and queue listings see only the tenant's records; `listQueued()` and `stats()`
33
+ * pass the tenant to the store as a filter, which Postgres and SQLite answer from an index.
34
+ */
35
+ export declare function tenantOperationStore<TResult = unknown>(store: OperationStore<TResult>, tenantId: string): OperationStore<TResult>;
36
+ /**
37
+ * One tenant's prompts. Every prompt name is placed under the tenant's prefix on the way in and
38
+ * taken off on the way out, so the tenant sees its own names and `listNames()` lists only them. A
39
+ * `ContextHub` given this store keeps the tenant's context bundles apart the same way.
40
+ */
41
+ export declare function tenantPromptStore(store: PromptStore, tenantId: string): PromptStore;
42
+ /** One tenant's datasets: names placed under the tenant's prefix, and `list()` showing only its own. */
43
+ export declare function tenantDatasetStore(store: DatasetStore, tenantId: string): DatasetStore;
44
+ /**
45
+ * One tenant's experiments. Ids, names, and dataset names are placed under the tenant's prefix, so
46
+ * `get()` of another tenant's experiment id finds nothing and `list()` returns only the tenant's.
47
+ */
48
+ export declare function tenantExperimentStore(store: ExperimentStore, tenantId: string): ExperimentStore;
49
+ /**
50
+ * One tenant's retrieval chunks. Each chunk's id is placed under the tenant's prefix and its metadata
51
+ * stamped `tenantId`, and every search adds `tenantId` to its filter, which every vector store applies
52
+ * natively. A search never ranks another tenant's chunks, and `delete()` reaches only the tenant's.
53
+ */
54
+ export declare function tenantVectorStore(store: VectorStore, tenantId: string): VectorStore;
55
+ /**
56
+ * One tenant's cache: every key placed under the tenant's prefix, so a tenant can never read what
57
+ * another cached. `clear()` is left out, since it would clear every tenant's entries.
58
+ */
59
+ export declare function tenantCache<T = unknown>(cache: CacheAdapter<T>, tenantId: string): CacheAdapter<T>;
60
+ /** An asset store with the tenant bound, so no call can name another. Returned by `tenantAssetStore()`. */
61
+ export interface TenantAssetStore {
62
+ /** The tenant every call acts for. */
63
+ readonly tenantId: string;
64
+ /** Stores an asset for the tenant. */
65
+ put(input: Parameters<AssetStore['put']>[0], options: Omit<AssetPutOptions, 'tenantId'>): ReturnType<AssetStore['put']>;
66
+ /** Reads one of the tenant's assets with its bytes. */
67
+ get(assetId: string): ReturnType<AssetStore['get']>;
68
+ /** Describes one of the tenant's assets. */
69
+ stat(assetId: string): ReturnType<AssetStore['stat']>;
70
+ /** Deletes one of the tenant's assets. */
71
+ delete(assetId: string): ReturnType<AssetStore['delete']>;
72
+ /** Signs a URL for one of the tenant's assets. */
73
+ sign(assetId: string, options?: AssetSignOptions): ReturnType<AssetStore['sign']>;
74
+ }
75
+ /**
76
+ * One tenant's assets. Asset stores already treat another tenant's asset as missing; this binds the
77
+ * tenant once, so code handed the view cannot pass a different one.
78
+ */
79
+ export declare function tenantAssetStore(store: AssetStore, tenantId: string): TenantAssetStore;
80
+ /** One tenant's rollups: rows it adds are counted for the tenant, and queries read only the tenant's rows. */
81
+ export declare function tenantRollupStore(store: RollupStore, tenantId: string): RollupStore;
82
+ /** The stores `tenantScope()` can scope. Each is optional. */
83
+ export interface TenantStores {
84
+ /** Long-term memory. */
85
+ store?: Store;
86
+ /** Traces. */
87
+ traces?: TraceStore;
88
+ /** Operation records. */
89
+ operations?: OperationStore<unknown>;
90
+ /** Prompt versions and labels, and the context hub's bundles. */
91
+ prompts?: PromptStore;
92
+ /** Datasets. */
93
+ datasets?: DatasetStore;
94
+ /** Experiments. */
95
+ experiments?: ExperimentStore;
96
+ /** Retrieval chunks. */
97
+ vectors?: VectorStore;
98
+ /** A cache. */
99
+ cache?: CacheAdapter<unknown>;
100
+ /** Dashboard rollups. */
101
+ rollups?: RollupStore;
102
+ }
103
+ /**
104
+ * Scopes every given store to one tenant at once, keeping each one's type, so a request handler
105
+ * builds its tenant's view in one line.
106
+ *
107
+ * ```ts
108
+ * const scoped = tenantScope(principal.tenantId, { store, traces, prompts, vectors });
109
+ * ```
110
+ */
111
+ export declare function tenantScope<T extends TenantStores>(tenantId: string, stores: T): T & {
112
+ readonly tenantId: string;
113
+ };
@@ -0,0 +1,233 @@
1
+ import { tenantPrefix } from '../utils/tenant.js';
2
+ import { tenantStore } from '../store/tenant.js';
3
+ export { TENANT_NAMESPACE, tenantStore } from '../store/tenant.js';
4
+ export { assertTenantId, TenantIdError } from '../utils/tenant.js';
5
+ function names(tenantId) {
6
+ const prefix = tenantPrefix(tenantId);
7
+ return {
8
+ prefix,
9
+ in: (name) => `${prefix}${name}`,
10
+ owns: (name) => name.startsWith(prefix),
11
+ out: (name) => name.slice(prefix.length),
12
+ };
13
+ }
14
+ function stamped(metadata, tenantId) {
15
+ return metadata?.tenantId === tenantId;
16
+ }
17
+ export function tenantTraceStore(store, tenantId) {
18
+ tenantPrefix(tenantId);
19
+ const own = (run) => (run && stamped(run.metadata, tenantId) ? run : undefined);
20
+ const prune = (tree) => ({
21
+ ...tree,
22
+ children: tree.children.filter((child) => stamped(child.metadata, tenantId)).map(prune),
23
+ });
24
+ return {
25
+ save: (run) => store.save({ ...run, metadata: { ...run.metadata, tenantId } }),
26
+ get: async (runId) => own(await store.get(runId)),
27
+ query: (query = {}) => store.query({ ...query, metadata: { ...query.metadata, tenantId } }),
28
+ async tree(traceId) {
29
+ const tree = await store.tree(traceId);
30
+ return tree && stamped(tree.metadata, tenantId) ? prune(tree) : undefined;
31
+ },
32
+ async addFeedback(runId, feedback) {
33
+ const run = own(await store.get(runId));
34
+ if (!run)
35
+ return;
36
+ if (store.addFeedback)
37
+ await store.addFeedback(runId, feedback);
38
+ else
39
+ await store.save({ ...run, feedback: [...(run.feedback ?? []), feedback] });
40
+ },
41
+ };
42
+ }
43
+ export function tenantOperationStore(store, tenantId) {
44
+ const keys = names(tenantId);
45
+ const inward = (record) => ({
46
+ ...record,
47
+ tenantId,
48
+ ...(record.idempotencyKey ? { idempotencyKey: keys.in(record.idempotencyKey) } : {}),
49
+ });
50
+ const outward = (record) => {
51
+ if (!record || record.tenantId !== tenantId)
52
+ return undefined;
53
+ return record.idempotencyKey && keys.owns(record.idempotencyKey)
54
+ ? { ...record, idempotencyKey: keys.out(record.idempotencyKey) }
55
+ : record;
56
+ };
57
+ const own = (records) => records.map(outward).filter((record) => record !== undefined);
58
+ const filtered = (filter) => ({ ...filter, tenantId });
59
+ const view = {
60
+ create: (record) => store.create(inward(record)),
61
+ read: async (id) => outward(await store.read(id)),
62
+ async update(record, expectedSequence) {
63
+ if (record.tenantId !== undefined && record.tenantId !== tenantId)
64
+ return false;
65
+ if (!outward(await store.read(record.id)))
66
+ return false;
67
+ return store.update(inward(record), expectedSequence);
68
+ },
69
+ };
70
+ if (store.delete) {
71
+ const remove = store.delete.bind(store);
72
+ view.delete = async (id) => (outward(await store.read(id)) ? remove(id) : false);
73
+ }
74
+ if (store.claimExpired) {
75
+ const claim = store.claimExpired.bind(store);
76
+ view.claimExpired = async (now, limit) => own(await claim(now, limit));
77
+ }
78
+ if (store.findByIdempotencyKey) {
79
+ const find = store.findByIdempotencyKey.bind(store);
80
+ view.findByIdempotencyKey = async (key) => outward(await find(keys.in(key)));
81
+ }
82
+ if (store.list) {
83
+ const list = store.list.bind(store);
84
+ view.list = async () => own(await list());
85
+ }
86
+ if (store.listQueued) {
87
+ const listQueued = store.listQueued.bind(store);
88
+ view.listQueued = async (limit, filter) => own(await listQueued(limit, filtered(filter)));
89
+ }
90
+ if (store.stats) {
91
+ const stats = store.stats.bind(store);
92
+ view.stats = (now, filter) => stats(now, filtered(filter));
93
+ }
94
+ return view;
95
+ }
96
+ export function tenantPromptStore(store, tenantId) {
97
+ const scoped = names(tenantId);
98
+ const named = (value) => ({ ...value, name: scoped.in(value.name) });
99
+ const unnamed = (value) => value && scoped.owns(value.name) ? { ...value, name: scoped.out(value.name) } : undefined;
100
+ const all = (values) => values.map(unnamed).filter((value) => value !== undefined);
101
+ return {
102
+ saveVersion: (version) => store.saveVersion(named(version)),
103
+ getVersion: async (name, version) => unnamed(await store.getVersion(scoped.in(name), version)),
104
+ listVersions: async (name, options) => all(await store.listVersions(scoped.in(name), options)),
105
+ getLabel: async (name, label) => unnamed(await store.getLabel(scoped.in(name), label)),
106
+ listLabels: async (name) => all(await store.listLabels(scoped.in(name))),
107
+ setLabel: (label, expected) => store.setLabel(named(label), expected),
108
+ deleteLabel: (name, label) => store.deleteLabel(scoped.in(name), label),
109
+ appendHistory: (entry) => store.appendHistory(named(entry)),
110
+ listHistory: async (name, options) => all(await store.listHistory(scoped.in(name), options)),
111
+ listNames: async () => (await store.listNames()).filter(scoped.owns).map(scoped.out).sort(),
112
+ };
113
+ }
114
+ export function tenantDatasetStore(store, tenantId) {
115
+ const scoped = names(tenantId);
116
+ return {
117
+ save: (dataset) => store.save({ ...dataset, name: scoped.in(dataset.name) }),
118
+ async get(name, version) {
119
+ const dataset = await store.get(scoped.in(name), version);
120
+ return dataset ? { ...dataset, name: name } : undefined;
121
+ },
122
+ async list() {
123
+ return (await store.list())
124
+ .filter((entry) => scoped.owns(entry.name))
125
+ .map((entry) => ({ ...entry, name: scoped.out(entry.name) }));
126
+ },
127
+ };
128
+ }
129
+ export function tenantExperimentStore(store, tenantId) {
130
+ const scoped = names(tenantId);
131
+ const inward = (experiment) => ({
132
+ ...experiment,
133
+ id: scoped.in(experiment.id),
134
+ name: scoped.in(experiment.name),
135
+ dataset: { ...experiment.dataset, name: scoped.in(experiment.dataset.name) },
136
+ });
137
+ const outward = (experiment) => experiment && scoped.owns(experiment.id)
138
+ ? {
139
+ ...experiment,
140
+ id: scoped.out(experiment.id),
141
+ name: scoped.out(experiment.name),
142
+ dataset: { ...experiment.dataset, name: scoped.out(experiment.dataset.name) },
143
+ }
144
+ : undefined;
145
+ return {
146
+ save: (experiment) => store.save(inward(experiment)),
147
+ get: async (id) => outward(await store.get(scoped.in(id))),
148
+ async list(filter = {}) {
149
+ const experiments = await store.list({
150
+ ...filter,
151
+ ...(filter.name === undefined ? {} : { name: scoped.in(filter.name) }),
152
+ ...(filter.dataset === undefined ? {} : { dataset: scoped.in(filter.dataset) }),
153
+ });
154
+ return experiments.map(outward).filter((experiment) => experiment !== undefined);
155
+ },
156
+ };
157
+ }
158
+ export function tenantVectorStore(store, tenantId) {
159
+ const scoped = names(tenantId);
160
+ const outward = (results) => results
161
+ .filter((result) => scoped.owns(result.id) && stamped(result.metadata, tenantId))
162
+ .map((result) => {
163
+ const { tenantId: _owner, ...metadata } = result.metadata ?? {};
164
+ return { ...result, id: scoped.out(result.id), metadata };
165
+ });
166
+ const filtered = (options = {}) => ({
167
+ ...options,
168
+ filter: { ...options.filter, tenantId },
169
+ });
170
+ return {
171
+ add: (documents) => store.add(documents.map((document) => ({
172
+ ...document,
173
+ id: scoped.in(document.id),
174
+ metadata: { ...document.metadata, tenantId },
175
+ }))),
176
+ search: async (query, options) => outward(await store.search(query, filtered(options))),
177
+ searchVector: async (vector, options) => outward(await store.searchVector(vector, filtered(options))),
178
+ delete: (ids) => store.delete(ids.map(scoped.in)),
179
+ };
180
+ }
181
+ export function tenantCache(cache, tenantId) {
182
+ const scoped = names(tenantId);
183
+ const view = {
184
+ get: (key) => cache.get(scoped.in(key)),
185
+ set: (key, value, ttlSeconds) => cache.set(scoped.in(key), value, ttlSeconds),
186
+ };
187
+ if (cache.delete) {
188
+ const remove = cache.delete.bind(cache);
189
+ view.delete = (key) => remove(scoped.in(key));
190
+ }
191
+ return view;
192
+ }
193
+ export function tenantAssetStore(store, tenantId) {
194
+ tenantPrefix(tenantId);
195
+ return {
196
+ tenantId,
197
+ put: (input, options) => store.put(input, { ...options, tenantId }),
198
+ get: (assetId) => store.get(assetId, tenantId),
199
+ stat: (assetId) => store.stat(assetId, tenantId),
200
+ delete: (assetId) => store.delete(assetId, tenantId),
201
+ sign: (assetId, options) => store.sign(assetId, tenantId, options),
202
+ };
203
+ }
204
+ export function tenantRollupStore(store, tenantId) {
205
+ tenantPrefix(tenantId);
206
+ return {
207
+ add: (key, totals) => store.add({ ...key, tenant: tenantId }, totals),
208
+ query: (query = {}) => store.query({ ...query, tenant: tenantId }),
209
+ };
210
+ }
211
+ export function tenantScope(tenantId, stores) {
212
+ tenantPrefix(tenantId);
213
+ const scoped = {};
214
+ if (stores.store)
215
+ scoped.store = tenantStore(stores.store, tenantId);
216
+ if (stores.traces)
217
+ scoped.traces = tenantTraceStore(stores.traces, tenantId);
218
+ if (stores.operations)
219
+ scoped.operations = tenantOperationStore(stores.operations, tenantId);
220
+ if (stores.prompts)
221
+ scoped.prompts = tenantPromptStore(stores.prompts, tenantId);
222
+ if (stores.datasets)
223
+ scoped.datasets = tenantDatasetStore(stores.datasets, tenantId);
224
+ if (stores.experiments)
225
+ scoped.experiments = tenantExperimentStore(stores.experiments, tenantId);
226
+ if (stores.vectors)
227
+ scoped.vectors = tenantVectorStore(stores.vectors, tenantId);
228
+ if (stores.cache)
229
+ scoped.cache = tenantCache(stores.cache, tenantId);
230
+ if (stores.rollups)
231
+ scoped.rollups = tenantRollupStore(stores.rollups, tenantId);
232
+ return { ...stores, ...scoped, tenantId };
233
+ }
@@ -140,6 +140,8 @@ export function traceModelClient(client, tracer, options = {}) {
140
140
  model: response.meta?.modelUsed,
141
141
  finishReason: response.finishReason,
142
142
  ...(request.metadata?.prompt === undefined ? {} : { prompt: request.metadata.prompt }),
143
+ ...(response.meta?.cache ? { cache: response.meta.cache } : {}),
144
+ ...(request.tenantId ? { tenantId: request.tenantId } : {}),
143
145
  },
144
146
  });
145
147
  return response;
@@ -8,6 +8,7 @@ import type { TelephonyConfig } from './telephony.js';
8
8
  import type { AliasMetadata, InputModality, ModelCapabilities, ModelStatus, OutputModality, RoutingModelPreference } from './providers.js';
9
9
  import type { CapabilityConfig } from './capabilities.js';
10
10
  import type { LifecycleConfig, OperationFamily } from './lifecycle.js';
11
+ import type { CompletionRequest } from './messages.js';
11
12
  import type { PipelineConfig } from '../pipeline/types.js';
12
13
  import type { CacheAdapter } from '../cache/adapters.js';
13
14
  import type { SemanticCacheOptions } from '../cache/semantic-cache.js';
@@ -228,6 +229,24 @@ export interface CacheConfig {
228
229
  strategy?: 'exact' | 'semantic' | 'hybrid';
229
230
  adapter?: CacheAdapter;
230
231
  semantic?: SemanticCacheOptions;
232
+ /**
233
+ * What cached responses are kept apart by, fixed or per request. A response is reused only within
234
+ * its namespace, so a new model revision or context version starts from an empty cache, and no
235
+ * tenant is answered from another's. The tenant defaults to the request's `tenantId`. Without it,
236
+ * exact-cache keys are what they were before 2.2; semantic matches are always kept to one tenant.
237
+ */
238
+ namespace?: CacheNamespace | ((request: CompletionRequest) => CacheNamespace);
239
+ }
240
+ /** What a cached response is kept apart by. Each field narrows the namespace. */
241
+ export interface CacheNamespace {
242
+ /** The tenant. Defaults to the request's `tenantId`. */
243
+ tenant?: string;
244
+ /** The deployment environment, such as `staging`. */
245
+ environment?: string;
246
+ /** A revision of the model or its prompts; changing it retires every earlier entry. */
247
+ modelRevision?: string;
248
+ /** The version of the context an answer depends on, such as a context hub bundle's. */
249
+ contextVersion?: string;
231
250
  }
232
251
  /** A response format applied to every request that does not set its own. */
233
252
  export interface ResponseFormatConfig {
@@ -244,8 +263,11 @@ export interface RateLimitConfig {
244
263
  maxRequests: number;
245
264
  /** Window length in milliseconds. */
246
265
  windowMs: number;
247
- /** What a budget is counted per: each user, each model, or everything together. */
248
- key?: 'userId' | 'model' | 'global';
266
+ /**
267
+ * What a budget is counted per: each user, each model, each tenant (the request's `tenantId`), or
268
+ * everything together. Per tenant, one tenant cannot spend another's share of a shared client.
269
+ */
270
+ key?: 'userId' | 'model' | 'tenantId' | 'global';
249
271
  /**
250
272
  * How calls are counted. `fixed-window` (the default) counts per window, so a burst at the end of
251
273
  * one window and another at the start of the next can pass twice the limit in a moment. `gcra` is
@@ -372,6 +372,8 @@ export interface NodeContext<S extends ChannelSchema> {
372
372
  readonly step: number;
373
373
  /** The thread this run belongs to. */
374
374
  readonly threadId: string;
375
+ /** The tenant the run is for, when it has one. */
376
+ readonly tenantId?: string;
375
377
  /** Identifies this task. Equal to `node` unless the task came from a `Send`. */
376
378
  readonly taskId: string;
377
379
  /** Input carried by a `Send`. Undefined for a node reached through an ordinary edge. */
@@ -395,7 +397,8 @@ export interface NodeContext<S extends ChannelSchema> {
395
397
  * Long-term memory, present when the graph was compiled with a store.
396
398
  *
397
399
  * State is this thread's; the store is everything else the application remembers — across threads,
398
- * users, and runs.
400
+ * users, and runs. In a run with a `tenantId`, it is that tenant's view of the store, and nothing
401
+ * another tenant stored can be read through it.
399
402
  */
400
403
  readonly store?: Store;
401
404
  /** Reports progress without writing to state. Refreshes an idle timeout. */
@@ -567,6 +570,13 @@ export interface GraphRunOptions {
567
570
  signal?: AbortSignal;
568
571
  /** Application data recorded on every checkpoint of the run. */
569
572
  metadata?: Record<string, unknown>;
573
+ /**
574
+ * The tenant the run is for. It is recorded on every checkpoint as `metadata.tenantId`, nodes read
575
+ * it as `context.tenantId`, and `context.store` becomes the tenant's own view of the store. A thread
576
+ * another tenant owns is not found: resuming, continuing, forking, or editing it with a different
577
+ * `tenantId` is refused before anything runs. A run without one carries on the thread's tenant.
578
+ */
579
+ tenantId?: string;
570
580
  /** Overrides the compiled `maxConcurrency` for this run. */
571
581
  maxConcurrency?: number;
572
582
  /** Breakpoints for this run only, replacing the compiled ones. */
@@ -257,6 +257,11 @@ export interface CompletionRequest {
257
257
  reasoning?: ReasoningConfig;
258
258
  /** Provider-side prompt caching controls. */
259
259
  cache?: PromptCacheConfig;
260
+ /**
261
+ * Skips this client's response cache for this request: `bypass` neither reads nor writes it, and
262
+ * `refresh` skips the read and stores the fresh answer. Either is recorded as a `bypass` outcome.
263
+ */
264
+ responseCache?: 'bypass' | 'refresh';
260
265
  /**
261
266
  * Overrides the configured capability policy for this request. `'strict'` fails on an option the
262
267
  * model cannot honor, `'warn'` drops it and records a warning, and `'off'` sends the request
@@ -173,6 +173,8 @@ export interface OperationRecord<TResult = unknown> {
173
173
  idempotencyKey?: string;
174
174
  /** Family and operation, such as `image.generate`, for filtering and metrics. */
175
175
  kind?: string;
176
+ /** The tenant it belongs to. A tenant-scoped store reads and lists only its own. */
177
+ tenantId?: string;
176
178
  /** Trace propagation headers, carried to the worker that runs it. */
177
179
  traceContext?: Record<string, string>;
178
180
  /** Application data. Must be serializable. */
@@ -217,6 +219,8 @@ export interface OperationStore<TResult = unknown> {
217
219
  export interface OperationStoreFilter {
218
220
  /** Only records whose `kind` starts with this, such as `assistant:` for the agent server's runs. */
219
221
  kindPrefix?: string;
222
+ /** Only records of this tenant. Postgres and SQLite read it through an index on queued work. */
223
+ tenantId?: string;
220
224
  }
221
225
  /** What an operation store holds, counted: the numbers a queue dashboard and an autoscaler read. */
222
226
  export interface OperationStoreStats {
@@ -305,6 +309,8 @@ export interface OperationSubmitOptions {
305
309
  id?: string;
306
310
  /** Family and operation, such as `image.generate`. */
307
311
  kind?: string;
312
+ /** The tenant it belongs to, recorded on the operation. */
313
+ tenantId?: string;
308
314
  /** Replays the operation already accepted under this key instead of starting another. */
309
315
  idempotencyKey?: string;
310
316
  /** Overrides the runner's attempt budget for this operation. */
@@ -72,6 +72,8 @@ export interface ResponseMeta {
72
72
  capabilityWarnings?: CapabilityWarning[];
73
73
  /** True when the response came from the response cache and no provider was called. */
74
74
  cacheHit: boolean;
75
+ /** What the response cache did for this request, and why. Present when the cache is enabled. */
76
+ cache?: CacheOutcome;
75
77
  /** Guardrails and techniques applied, such as redaction, trimming, or a retry. */
76
78
  guardrailsApplied: string[];
77
79
  /** Why the router chose the provider, and how many fallbacks were available. */
@@ -164,3 +166,21 @@ export interface NexusStream extends AsyncIterable<StreamChunk> {
164
166
  /** Stops the stream and the provider request behind it. */
165
167
  abort(): void;
166
168
  }
169
+ /**
170
+ * What the response cache did for one request: `hit` (answered from it), `miss` (looked, found
171
+ * nothing), `stale` (found an entry that had expired), `bypass` (the request skipped the lookup), or
172
+ * `error` (the cache failed, and the request went to the provider as on a miss). Recorded on
173
+ * `response.meta.cache` and on the request's trace.
174
+ */
175
+ export interface CacheOutcome {
176
+ /** What happened. */
177
+ outcome: 'hit' | 'miss' | 'stale' | 'bypass' | 'error';
178
+ /** Which layer answered or was asked last: the exact cache or the semantic one. */
179
+ layer?: 'exact' | 'semantic';
180
+ /** Why, in a few words, such as `no entry for this request` or the cache's error message. */
181
+ reason: string;
182
+ /** The namespace the lookup ran in, when one applies. */
183
+ namespace?: string;
184
+ /** Set when storing the response failed; the response was still returned. */
185
+ writeError?: string;
186
+ }