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
@@ -1,35 +1,69 @@
1
- import { circuitStoreMigration } from './circuits.js';
2
- import { evaluationStoreMigration } from './evaluate.js';
3
- import { operationStoreMigration } from './operations.js';
4
- import { promptStoreMigration } from './prompts.js';
5
- import { storeMigration } from './store.js';
6
- import { traceStoreMigration } from './traces.js';
7
- import { vectorStoreMigration } from './vectors.js';
8
- export { circuitStoreMigration, PostgresCircuitStateStore, } from './circuits.js';
1
+ import { MIGRATIONS_TABLE, migrationChecksum, sqlLiteral } from '../utils/schema-migrations.js';
2
+ import { circuitStoreMigrations } from './circuits.js';
3
+ import { quoteTable } from './client.js';
4
+ import { evaluationStoreMigrations } from './evaluate.js';
5
+ import { migrationsTableStatements } from './migrations.js';
6
+ import { operationStoreMigrations } from './operations.js';
7
+ import { promptStoreMigrations } from './prompts.js';
8
+ import { rollupStoreMigrations } from './rollups.js';
9
+ import { storeMigrations } from './store.js';
10
+ import { traceStoreMigrations } from './traces.js';
11
+ import { vectorStoreMigrations } from './vectors.js';
12
+ export { migrationChecksum, SchemaMigrationError, } from '../utils/schema-migrations.js';
13
+ export { circuitStoreMigration, circuitStoreMigrations, PostgresCircuitStateStore, } from './circuits.js';
9
14
  export { fromPostgresJs } from './client.js';
10
- export { evaluationStoreMigration, PostgresDatasetStore, PostgresExperimentStore, } from './evaluate.js';
11
- export { operationStoreMigration, PostgresOperationStore } from './operations.js';
12
- export { PostgresPromptStore, promptStoreMigration } from './prompts.js';
13
- export { PostgresStore, storeMigration } from './store.js';
14
- export { PostgresTraceStore, traceStoreMigration } from './traces.js';
15
- export { PostgresVectorStore, vectorStoreMigration } from './vectors.js';
15
+ export { evaluationStoreMigration, evaluationStoreMigrations, PostgresDatasetStore, PostgresExperimentStore, } from './evaluate.js';
16
+ export { applyPostgresMigrations, postgresMigrationStatus, } from './migrations.js';
17
+ export { operationStoreMigration, operationStoreMigrations, PostgresOperationStore, } from './operations.js';
18
+ export { PostgresPromptStore, promptStoreMigration, promptStoreMigrations, } from './prompts.js';
19
+ export { PostgresRollupStore, rollupStoreMigration, rollupStoreMigrations, } from './rollups.js';
20
+ export { PostgresStore, storeMigration, storeMigrations } from './store.js';
21
+ export { PostgresTraceStore, traceStoreMigration, traceStoreMigrations, } from './traces.js';
22
+ export { PostgresVectorStore, vectorStoreMigration, vectorStoreMigrations, } from './vectors.js';
23
+ export const POSTGRES_ADAPTERS = [
24
+ 'operations',
25
+ 'store',
26
+ 'traces',
27
+ 'evaluation',
28
+ 'circuits',
29
+ 'prompts',
30
+ 'rollups',
31
+ 'vectors',
32
+ ];
33
+ export function postgresMigrations(options = {}) {
34
+ const adapters = new Set(options.adapters ?? POSTGRES_ADAPTERS.filter((adapter) => adapter !== 'vectors'));
35
+ const vectorDimensions = options.vectorDimensions;
36
+ const migrationsOf = {
37
+ operations: () => operationStoreMigrations(),
38
+ store: () => storeMigrations(vectorDimensions ? { vectorDimensions } : {}),
39
+ traces: () => traceStoreMigrations(),
40
+ evaluation: () => evaluationStoreMigrations(),
41
+ circuits: () => circuitStoreMigrations(),
42
+ prompts: () => promptStoreMigrations(),
43
+ rollups: () => rollupStoreMigrations(),
44
+ vectors: () => {
45
+ if (vectorDimensions === undefined)
46
+ throw new RangeError('The vectors adapter needs vectorDimensions');
47
+ return vectorStoreMigrations({ dimensions: vectorDimensions });
48
+ },
49
+ };
50
+ return POSTGRES_ADAPTERS.filter((adapter) => adapters.has(adapter)).flatMap((adapter) => migrationsOf[adapter]());
51
+ }
16
52
  export function postgresMigration(options = {}) {
17
- const adapters = new Set(options.adapters ?? ['operations', 'store', 'traces', 'evaluation', 'circuits', 'prompts']);
18
- const statements = [
19
- ...(adapters.has('operations') ? operationStoreMigration() : []),
20
- ...(adapters.has('store')
21
- ? storeMigration(options.vectorDimensions ? { vectorDimensions: options.vectorDimensions } : {})
22
- : []),
23
- ...(adapters.has('traces') ? traceStoreMigration() : []),
24
- ...(adapters.has('evaluation') ? evaluationStoreMigration() : []),
25
- ...(adapters.has('circuits') ? circuitStoreMigration() : []),
26
- ...(adapters.has('prompts') ? promptStoreMigration() : []),
27
- ...(adapters.has('vectors') ? vectorMigrationFor(options.vectorDimensions) : []),
28
- ];
53
+ const migrations = postgresMigrations(options);
54
+ const statements = migrations.flatMap((migration) => [...migration.statements]);
55
+ if (options.record) {
56
+ statements.push(...migrationsTableStatements());
57
+ for (const migration of migrations) {
58
+ statements.push(`INSERT INTO ${quoteTable(MIGRATIONS_TABLE)} (component, version, name, checksum, applied_at) VALUES (${[
59
+ migration.component,
60
+ String(migration.version),
61
+ migration.name,
62
+ migrationChecksum(migration),
63
+ ]
64
+ .map((value, index) => (index === 1 ? value : sqlLiteral(value)))
65
+ .join(', ')}, to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) ON CONFLICT (component, version) DO NOTHING`);
66
+ }
67
+ }
29
68
  return `${statements.join(';\n\n')};\n`;
30
69
  }
31
- function vectorMigrationFor(dimensions) {
32
- if (dimensions === undefined)
33
- throw new RangeError('The vectors adapter needs vectorDimensions');
34
- return vectorStoreMigration({ dimensions });
35
- }
@@ -0,0 +1,60 @@
1
+ import { type MigrationResult, type MigrationStatus, type SchemaMigration } from '../utils/schema-migrations.js';
2
+ import { type PostgresLikeClient } from './client.js';
3
+ export { type AppliedMigration, type MigrationResult, type MigrationStatus, migrationChecksum, type SchemaMigration, SchemaMigrationError, } from '../utils/schema-migrations.js';
4
+ /** Options for applying or inspecting Postgres migrations. */
5
+ export interface PostgresMigrateOptions {
6
+ /** Plans without writing anything, not even the table that records migrations. */
7
+ dryRun?: boolean;
8
+ /** The table that records applied migrations, optionally schema-qualified. Defaults to `nexus_schema_migrations`. */
9
+ table?: string;
10
+ /**
11
+ * Runs one migration's statements, and the row recording it, in one transaction on one connection.
12
+ * Without it, statements run one at a time and the record is written last: every bundled statement
13
+ * is idempotent, so an interrupted migration runs again on the next call. Pass it for a pool, where
14
+ * `BEGIN` and `COMMIT` would otherwise reach different connections:
15
+ *
16
+ * ```ts
17
+ * transaction: async (run) => {
18
+ * const connection = await pool.connect();
19
+ * try {
20
+ * await connection.query('BEGIN');
21
+ * const result = await run(connection);
22
+ * await connection.query('COMMIT');
23
+ * return result;
24
+ * } catch (error) {
25
+ * await connection.query('ROLLBACK');
26
+ * throw error;
27
+ * } finally {
28
+ * connection.release();
29
+ * }
30
+ * }
31
+ * ```
32
+ *
33
+ * PGlite needs only `transaction: (run) => db.transaction(run)`.
34
+ */
35
+ transaction?: <T>(run: (client: PostgresLikeClient) => Promise<T>) => Promise<T>;
36
+ /**
37
+ * How long one migrator holds the lock before another may take it over, renewed before each
38
+ * migration. Defaults to 10 minutes, longer than any bundled migration takes.
39
+ */
40
+ lockTtlMs?: number;
41
+ /** How long to wait for another migrator to finish before giving up. Defaults to 2 minutes. */
42
+ lockTimeoutMs?: number;
43
+ }
44
+ /**
45
+ * Where a Postgres database stands against a list of migrations. Reads only: on a database that has
46
+ * never been migrated, everything is pending.
47
+ */
48
+ export declare function postgresMigrationStatus(client: PostgresLikeClient, migrations: readonly SchemaMigration[], options?: Pick<PostgresMigrateOptions, 'table'>): Promise<MigrationStatus>;
49
+ /**
50
+ * Applies the pending migrations in order, under a lock, recording each with a checksum.
51
+ *
52
+ * The lock is a row with a lease rather than a session advisory lock, so it holds through a
53
+ * connection pool and serverless drivers alike: a second migrator waits, and one that died is taken
54
+ * over once its lease lapses. A migration whose statements changed after it ran is refused before
55
+ * anything is written. Migrations a newer release recorded are left alone, so an older worker
56
+ * starting mid-rollout does nothing.
57
+ */
58
+ export declare function applyPostgresMigrations(client: PostgresLikeClient, migrations: readonly SchemaMigration[], options?: PostgresMigrateOptions): Promise<MigrationResult>;
59
+ /** The statements that create the migrations table and its lock, which `migrate()` runs first. */
60
+ export declare function migrationsTableStatements(table?: string): string[];
@@ -0,0 +1,121 @@
1
+ import { assertUnchanged, delay, label, MIGRATIONS_TABLE, migrationChecksum, planMigrations, SchemaMigrationError, } from '../utils/schema-migrations.js';
2
+ import { quoteTable } from './client.js';
3
+ export { migrationChecksum, SchemaMigrationError, } from '../utils/schema-migrations.js';
4
+ const LOCK_NAME = 'migrate';
5
+ const DB_NOW = '(extract(epoch from clock_timestamp()) * 1000)';
6
+ export async function postgresMigrationStatus(client, migrations, options = {}) {
7
+ return planMigrations(migrations, await readApplied(client, options.table ?? MIGRATIONS_TABLE));
8
+ }
9
+ export async function applyPostgresMigrations(client, migrations, options = {}) {
10
+ const tableName = options.table ?? MIGRATIONS_TABLE;
11
+ if (options.dryRun) {
12
+ const status = await postgresMigrationStatus(client, migrations, options);
13
+ assertUnchanged(status);
14
+ return { applied: status.pending, statements: status.pending.flatMap((m) => [...m.statements]), dryRun: true };
15
+ }
16
+ planMigrations(migrations, []);
17
+ await bootstrap(client, tableName);
18
+ const owner = `migrator-${globalThis.crypto.randomUUID()}`;
19
+ const lockTtlMs = options.lockTtlMs ?? 600_000;
20
+ await acquire(client, tableName, owner, lockTtlMs, options.lockTimeoutMs ?? 120_000);
21
+ try {
22
+ const status = planMigrations(migrations, await readApplied(client, tableName));
23
+ assertUnchanged(status);
24
+ const statements = [];
25
+ for (const migration of status.pending) {
26
+ if (!(await claim(client, tableName, owner, lockTtlMs))) {
27
+ throw new SchemaMigrationError('MIGRATION_LOCKED', `The migration lock lapsed before ${label(migration)}`);
28
+ }
29
+ const apply = async (connection) => {
30
+ for (const statement of migration.statements)
31
+ await connection.query(statement);
32
+ await connection.query(`INSERT INTO ${quoteTable(tableName)} (component, version, name, checksum, applied_at)
33
+ VALUES ($1, $2, $3, $4, $5) ON CONFLICT (component, version) DO NOTHING`, [
34
+ migration.component,
35
+ migration.version,
36
+ migration.name,
37
+ migrationChecksum(migration),
38
+ new Date().toISOString(),
39
+ ]);
40
+ };
41
+ if (options.transaction)
42
+ await options.transaction(apply);
43
+ else
44
+ await apply(client);
45
+ statements.push(...migration.statements);
46
+ }
47
+ return { applied: status.pending, statements, dryRun: false };
48
+ }
49
+ finally {
50
+ await client
51
+ .query(`DELETE FROM ${lockTable(tableName)} WHERE name = $1 AND owner = $2`, [LOCK_NAME, owner])
52
+ .catch(() => undefined);
53
+ }
54
+ }
55
+ export function migrationsTableStatements(table = MIGRATIONS_TABLE) {
56
+ return [
57
+ `CREATE TABLE IF NOT EXISTS ${quoteTable(table)} (
58
+ component text NOT NULL,
59
+ version integer NOT NULL,
60
+ name text NOT NULL,
61
+ checksum text NOT NULL,
62
+ applied_at text COLLATE "C" NOT NULL,
63
+ PRIMARY KEY (component, version)
64
+ )`,
65
+ `CREATE TABLE IF NOT EXISTS ${lockTable(table)} (
66
+ name text PRIMARY KEY,
67
+ owner text NOT NULL,
68
+ expires_at double precision NOT NULL
69
+ )`,
70
+ ];
71
+ }
72
+ async function bootstrap(client, table) {
73
+ for (const statement of migrationsTableStatements(table)) {
74
+ try {
75
+ await client.query(statement);
76
+ }
77
+ catch (error) {
78
+ const code = error?.code;
79
+ if (code !== '23505' && code !== '42P07')
80
+ throw error;
81
+ }
82
+ }
83
+ }
84
+ async function readApplied(client, table) {
85
+ const { rows: exists } = await client.query('SELECT to_regclass($1) IS NOT NULL AS present', [quoteTable(table)]);
86
+ if (!exists[0]?.present)
87
+ return [];
88
+ const { rows } = await client.query(`SELECT component, version, name, checksum, applied_at FROM ${quoteTable(table)} ORDER BY applied_at, component, version`);
89
+ return rows.map((row) => {
90
+ const value = row;
91
+ return {
92
+ component: String(value.component),
93
+ version: Number(value.version),
94
+ name: String(value.name),
95
+ checksum: String(value.checksum),
96
+ appliedAt: String(value.applied_at),
97
+ };
98
+ });
99
+ }
100
+ async function acquire(client, table, owner, ttlMs, timeoutMs) {
101
+ const deadline = Date.now() + timeoutMs;
102
+ let wait = 50;
103
+ while (!(await claim(client, table, owner, ttlMs))) {
104
+ if (Date.now() >= deadline) {
105
+ throw new SchemaMigrationError('MIGRATION_LOCKED', `Another migrator held the lock for ${timeoutMs} ms. It is released when that migrator finishes, or when its lease lapses.`);
106
+ }
107
+ await delay(wait);
108
+ wait = Math.min(wait * 2, 1_000);
109
+ }
110
+ }
111
+ async function claim(client, table, owner, ttlMs) {
112
+ const lock = lockTable(table);
113
+ const { rows } = await client.query(`INSERT INTO ${lock} (name, owner, expires_at) VALUES ($1, $2, ${DB_NOW} + $3::double precision)
114
+ ON CONFLICT (name) DO UPDATE SET owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at
115
+ WHERE ${lock}.owner = EXCLUDED.owner OR ${lock}.expires_at < ${DB_NOW}
116
+ RETURNING owner`, [LOCK_NAME, owner, ttlMs]);
117
+ return rows.length > 0;
118
+ }
119
+ function lockTable(table) {
120
+ return quoteTable(`${table}_lock`);
121
+ }
@@ -1,12 +1,20 @@
1
1
  import type { OperationRecord, OperationStore, OperationStoreFilter, OperationStoreStats } from '../types/operations.js';
2
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
2
3
  import { type PostgresLikeClient } from './client.js';
4
+ import type { PostgresMigrateOptions } from './migrations.js';
3
5
  /** Options for the Postgres operation store. */
4
6
  export interface PostgresOperationStoreOptions {
5
7
  /** Table name, optionally schema-qualified. Defaults to `nexus_operations`. */
6
8
  table?: string;
7
9
  }
8
- /** The schema, as statements. Applied by `migrate()`, `nexus db sql`, or the application's own tooling. */
10
+ /** The schema, as statements: every migration's, in order. For `nexus db sql` or the application's own tooling. */
9
11
  export declare function operationStoreMigration(options?: PostgresOperationStoreOptions): string[];
12
+ /**
13
+ * The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 table;
14
+ * version 2 indexes queued work by age, so a claim reads the oldest few rows instead of sorting the
15
+ * queue; version 3 indexes queued work by tenant.
16
+ */
17
+ export declare function operationStoreMigrations(options?: PostgresOperationStoreOptions): SchemaMigration[];
10
18
  /**
11
19
  * Operation records in Postgres.
12
20
  *
@@ -25,8 +33,11 @@ export declare class PostgresOperationStore<TResult = unknown> implements Operat
25
33
  private readonly options;
26
34
  private readonly table;
27
35
  constructor(client: PostgresLikeClient, options?: PostgresOperationStoreOptions);
28
- /** Creates the table and indexes if they do not exist. Never runs implicitly. */
29
- migrate(): Promise<void>;
36
+ /**
37
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
38
+ * Never runs implicitly. Safe on a database an older release created: what exists is kept.
39
+ */
40
+ migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
30
41
  /**
31
42
  * Inserts a new record. Throws `OperationDuplicateError` when its idempotency key is already
32
43
  * claimed. Refuses records carrying raw bytes.
@@ -1,12 +1,37 @@
1
1
  import { OperationDuplicateError } from '../operations/errors.js';
2
2
  import { assertSerializableRecord } from '../operations/serialization.js';
3
3
  import { TERMINAL_OPERATION_STATUSES } from '../types/operations.js';
4
- import { fromJson, isUniqueViolation, quoteDerived, quoteTable, runStatements, } from './client.js';
4
+ import { fromJson, isUniqueViolation, quoteDerived, quoteTable } from './client.js';
5
5
  const DEFAULT_TABLE = 'nexus_operations';
6
6
  const TERMINAL = TERMINAL_OPERATION_STATUSES.map((status) => `'${status}'`).join(', ');
7
7
  export function operationStoreMigration(options = {}) {
8
+ return operationStoreMigrations(options).flatMap((migration) => [...migration.statements]);
9
+ }
10
+ export function operationStoreMigrations(options = {}) {
8
11
  const name = options.table ?? DEFAULT_TABLE;
9
12
  const table = quoteTable(name);
13
+ const component = `operations:${name}`;
14
+ return [
15
+ { component, version: 1, name: 'create the operations table', statements: createStatements(name, table) },
16
+ {
17
+ component,
18
+ version: 2,
19
+ name: 'index queued work by age',
20
+ statements: [
21
+ `CREATE INDEX IF NOT EXISTS ${quoteDerived(name, 'queued')} ON ${table} (created_at, id) WHERE status = 'queued'`,
22
+ ],
23
+ },
24
+ {
25
+ component,
26
+ version: 3,
27
+ name: 'index queued work by tenant',
28
+ statements: [
29
+ `CREATE INDEX IF NOT EXISTS ${quoteDerived(name, 'tenant_queued')} ON ${table} ((doc ->> 'tenantId'), created_at, id) WHERE status = 'queued'`,
30
+ ],
31
+ },
32
+ ];
33
+ }
34
+ function createStatements(name, table) {
10
35
  return [
11
36
  `CREATE TABLE IF NOT EXISTS ${table} (
12
37
  id text PRIMARY KEY,
@@ -32,8 +57,9 @@ export class PostgresOperationStore {
32
57
  this.options = options;
33
58
  this.table = quoteTable(options.table ?? DEFAULT_TABLE);
34
59
  }
35
- async migrate() {
36
- await runStatements(this.client, operationStoreMigration(this.options));
60
+ async migrate(options = {}) {
61
+ const { applyPostgresMigrations } = await import('./migrations.js');
62
+ return applyPostgresMigrations(this.client, operationStoreMigrations(this.options), options);
37
63
  }
38
64
  async create(record) {
39
65
  assertSerializableRecord(record);
@@ -95,29 +121,21 @@ export class PostgresOperationStore {
95
121
  }
96
122
  async listQueued(limit, filter) {
97
123
  const params = [new Date().toISOString(), limit];
98
- let kind = '';
99
- if (filter?.kindPrefix) {
100
- params.push(filter.kindPrefix);
101
- kind = ' AND substr(kind, 1, char_length($3)) = $3';
102
- }
124
+ const narrowed = filterSql(filter, params);
103
125
  const { rows } = await this.client.query(`SELECT doc::text AS doc FROM ${this.table}
104
- WHERE status = 'queued' AND (lease_expires_at IS NULL OR lease_expires_at <= $1)${kind}
126
+ WHERE status = 'queued' AND (lease_expires_at IS NULL OR lease_expires_at <= $1)${narrowed}
105
127
  ORDER BY created_at, id
106
128
  LIMIT $2`, params);
107
129
  return rows.map((row) => fromJson(row.doc));
108
130
  }
109
131
  async stats(now, filter) {
110
132
  const params = [now];
111
- let kind = '';
112
- if (filter?.kindPrefix) {
113
- params.push(filter.kindPrefix);
114
- kind = ' AND substr(kind, 1, char_length($2)) = $2';
115
- }
133
+ const narrowed = filterSql(filter, params);
116
134
  const { rows } = await this.client.query(`SELECT status, count(*)::int AS count,
117
135
  min(created_at) FILTER (WHERE status = 'queued') AS oldest,
118
136
  count(*) FILTER (WHERE status = 'running' AND (lease_expires_at IS NULL OR lease_expires_at <= $1))::int AS lapsed
119
137
  FROM ${this.table}
120
- WHERE status NOT IN (${TERMINAL})${kind}
138
+ WHERE status NOT IN (${TERMINAL})${narrowed}
121
139
  GROUP BY status`, params);
122
140
  return statsFromRows(rows);
123
141
  }
@@ -139,6 +157,18 @@ export class PostgresOperationStore {
139
157
  ];
140
158
  }
141
159
  }
160
+ function filterSql(filter, params) {
161
+ let sql = '';
162
+ if (filter?.kindPrefix) {
163
+ params.push(filter.kindPrefix);
164
+ sql += ` AND substr(kind, 1, char_length($${params.length})) = $${params.length}`;
165
+ }
166
+ if (filter?.tenantId !== undefined) {
167
+ params.push(filter.tenantId);
168
+ sql += ` AND doc ->> 'tenantId' = $${params.length}`;
169
+ }
170
+ return sql;
171
+ }
142
172
  function statsFromRows(rows) {
143
173
  const byStatus = {};
144
174
  let oldestQueuedAt;
@@ -1,10 +1,16 @@
1
1
  import type { PromptHistoryEntry, PromptLabel, PromptStore, PromptVersion } from '../types/prompts.js';
2
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
2
3
  import { type PostgresLikeClient } from './client.js';
4
+ import type { PostgresMigrateOptions } from './migrations.js';
3
5
  /** Options for the Postgres prompt store. */
4
6
  export interface PostgresPromptStoreOptions {
5
7
  /** Table name prefix, optionally schema-qualified. Defaults to `nexus_prompt`, for `nexus_prompt_versions` and so on. */
6
8
  table?: string;
7
9
  }
10
+ /**
11
+ * The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 schema.
12
+ */
13
+ export declare function promptStoreMigrations(options?: PostgresPromptStoreOptions): SchemaMigration[];
8
14
  /** The schema for the prompt store, as statements. */
9
15
  export declare function promptStoreMigration(options?: PostgresPromptStoreOptions): string[];
10
16
  /**
@@ -20,8 +26,11 @@ export declare class PostgresPromptStore implements PromptStore {
20
26
  private readonly labels;
21
27
  private readonly history;
22
28
  constructor(client: PostgresLikeClient, options?: PostgresPromptStoreOptions);
23
- /** Creates the prompt tables if they do not exist. Never runs implicitly. */
24
- migrate(): Promise<void>;
29
+ /**
30
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
31
+ * Never runs implicitly. Safe on a database an older release created: what exists is kept.
32
+ */
33
+ migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
25
34
  /** Stores a version, unless one with the same content version exists. */
26
35
  saveVersion(version: PromptVersion): Promise<void>;
27
36
  /** Reads a version. */
@@ -1,8 +1,18 @@
1
- import { fromJson, quoteDerived, quoteTable, runStatements } from './client.js';
1
+ import { fromJson, quoteDerived, quoteTable } from './client.js';
2
2
  function tables(options) {
3
3
  const base = options.table ?? 'nexus_prompt';
4
4
  return { base, versions: `${base}_versions`, labels: `${base}_labels`, history: `${base}_history` };
5
5
  }
6
+ export function promptStoreMigrations(options = {}) {
7
+ return [
8
+ {
9
+ component: `prompts:${tables(options).versions}`,
10
+ version: 1,
11
+ name: 'create the prompt tables',
12
+ statements: promptStoreMigration(options),
13
+ },
14
+ ];
15
+ }
6
16
  export function promptStoreMigration(options = {}) {
7
17
  const names = tables(options);
8
18
  return [
@@ -44,8 +54,9 @@ export class PostgresPromptStore {
44
54
  this.labels = quoteTable(names.labels);
45
55
  this.history = quoteTable(names.history);
46
56
  }
47
- async migrate() {
48
- await runStatements(this.client, promptStoreMigration(this.options));
57
+ async migrate(options = {}) {
58
+ const { applyPostgresMigrations } = await import('./migrations.js');
59
+ return applyPostgresMigrations(this.client, promptStoreMigrations(this.options), options);
49
60
  }
50
61
  async saveVersion(version) {
51
62
  await this.client.query(`INSERT INTO ${this.versions} (name, version, created_at, doc) VALUES ($1, $2, $3, $4::jsonb) ON CONFLICT DO NOTHING`, [version.name, version.version, version.createdAt, JSON.stringify(version)]);
@@ -0,0 +1,35 @@
1
+ import { type RollupKey, type RollupQuery, type RollupRow, type RollupStore, type RollupTotals } from '../tracing/rollups.js';
2
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
3
+ import { type PostgresLikeClient } from './client.js';
4
+ import type { PostgresMigrateOptions } from './migrations.js';
5
+ /** Options for the Postgres rollup store. */
6
+ export interface PostgresRollupStoreOptions {
7
+ /** Table name, optionally schema-qualified. Defaults to `nexus_rollups`. */
8
+ table?: string;
9
+ }
10
+ /** The schema, as statements: every migration's, in order. */
11
+ export declare function rollupStoreMigration(options?: PostgresRollupStoreOptions): string[];
12
+ /** The versioned schema, which `migrate()` and `nexus db migrate` apply. */
13
+ export declare function rollupStoreMigrations(options?: PostgresRollupStoreOptions): SchemaMigration[];
14
+ /**
15
+ * Rollup rows in Postgres, so a dashboard's totals live beside its traces and survive a restart.
16
+ *
17
+ * Adding to a row is one `INSERT … ON CONFLICT DO UPDATE` that sums every total, latency buckets
18
+ * included, so two workers finishing runs in the same hour both count. A query reads only the rows
19
+ * of its range: a month of one tenant is a few hundred rows, however many runs it held.
20
+ */
21
+ export declare class PostgresRollupStore implements RollupStore {
22
+ private readonly client;
23
+ private readonly options;
24
+ private readonly table;
25
+ constructor(client: PostgresLikeClient, options?: PostgresRollupStoreOptions);
26
+ /**
27
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
28
+ * Never runs implicitly.
29
+ */
30
+ migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
31
+ /** Adds totals to the row for `key`, creating it, in one statement. */
32
+ add(key: RollupKey, totals: RollupTotals): Promise<void>;
33
+ /** Rows matching a query, oldest hour first, filtered as the in-memory store filters them. */
34
+ query(query?: RollupQuery): Promise<RollupRow[]>;
35
+ }
@@ -0,0 +1,129 @@
1
+ import { ROLLUP_LATENCY_BOUNDS_MS, } from '../tracing/rollups.js';
2
+ import { quoteDerived, quoteTable } from './client.js';
3
+ import { SqlParams } from './sql.js';
4
+ const DEFAULT_TABLE = 'nexus_rollups';
5
+ export function rollupStoreMigration(options = {}) {
6
+ return rollupStoreMigrations(options).flatMap((migration) => [...migration.statements]);
7
+ }
8
+ export function rollupStoreMigrations(options = {}) {
9
+ const name = options.table ?? DEFAULT_TABLE;
10
+ const table = quoteTable(name);
11
+ return [
12
+ {
13
+ component: `rollups:${name}`,
14
+ version: 1,
15
+ name: 'create the rollups table',
16
+ statements: [
17
+ `CREATE TABLE IF NOT EXISTS ${table} (
18
+ hour text COLLATE "C" NOT NULL,
19
+ kind text NOT NULL,
20
+ name text NOT NULL,
21
+ model text NOT NULL,
22
+ provider text NOT NULL,
23
+ tenant text NOT NULL,
24
+ runs bigint NOT NULL,
25
+ errors bigint NOT NULL,
26
+ cost double precision NOT NULL,
27
+ input_tokens bigint NOT NULL,
28
+ output_tokens bigint NOT NULL,
29
+ latency bigint[] NOT NULL,
30
+ PRIMARY KEY (hour, kind, name, model, provider, tenant)
31
+ )`,
32
+ `CREATE INDEX IF NOT EXISTS ${quoteDerived(name, 'tenant_hour')} ON ${table} (tenant, hour)`,
33
+ ],
34
+ },
35
+ ];
36
+ }
37
+ export class PostgresRollupStore {
38
+ client;
39
+ options;
40
+ table;
41
+ constructor(client, options = {}) {
42
+ this.client = client;
43
+ this.options = options;
44
+ this.table = quoteTable(options.table ?? DEFAULT_TABLE);
45
+ }
46
+ async migrate(options = {}) {
47
+ const { applyPostgresMigrations } = await import('./migrations.js');
48
+ return applyPostgresMigrations(this.client, rollupStoreMigrations(this.options), options);
49
+ }
50
+ async add(key, totals) {
51
+ const t = this.table;
52
+ await this.client.query(`INSERT INTO ${t} (hour, kind, name, model, provider, tenant, runs, errors, cost, input_tokens, output_tokens, latency)
53
+ VALUES ($1, $2, $3, $4, $5, $6, $7::bigint, $8::bigint, $9::double precision, $10::bigint, $11::bigint, $12::bigint[])
54
+ ON CONFLICT (hour, kind, name, model, provider, tenant) DO UPDATE SET
55
+ runs = ${t}.runs + EXCLUDED.runs,
56
+ errors = ${t}.errors + EXCLUDED.errors,
57
+ cost = ${t}.cost + EXCLUDED.cost,
58
+ input_tokens = ${t}.input_tokens + EXCLUDED.input_tokens,
59
+ output_tokens = ${t}.output_tokens + EXCLUDED.output_tokens,
60
+ latency = ARRAY(
61
+ SELECT coalesce(stored, 0) + coalesce(added, 0)
62
+ FROM unnest(${t}.latency, EXCLUDED.latency) WITH ORDINALITY AS bucket(stored, added, position)
63
+ ORDER BY position
64
+ )`, [
65
+ key.hour,
66
+ key.kind,
67
+ key.name,
68
+ key.model,
69
+ key.provider,
70
+ key.tenant,
71
+ totals.runs,
72
+ totals.errors,
73
+ totals.cost,
74
+ totals.inputTokens,
75
+ totals.outputTokens,
76
+ `{${bucketsOf(totals.latency).join(',')}}`,
77
+ ]);
78
+ }
79
+ async query(query = {}) {
80
+ const params = new SqlParams();
81
+ const where = [];
82
+ if (query.since)
83
+ where.push(`hour >= ${params.add(hourOf(query.since))}`);
84
+ if (query.until)
85
+ where.push(`hour <= ${params.add(query.until)}`);
86
+ if (query.kind !== undefined) {
87
+ const kinds = Array.isArray(query.kind) ? query.kind : [query.kind];
88
+ where.push(kinds.length ? `kind IN (${kinds.map((kind) => params.add(kind)).join(', ')})` : 'FALSE');
89
+ }
90
+ if (query.model !== undefined)
91
+ where.push(`model = ${params.add(query.model)}`);
92
+ if (query.provider !== undefined)
93
+ where.push(`provider = ${params.add(query.provider)}`);
94
+ if (query.tenant !== undefined)
95
+ where.push(`tenant = ${params.add(query.tenant)}`);
96
+ if (query.name !== undefined)
97
+ where.push(`name = ${params.add(query.name)}`);
98
+ const { rows } = await this.client.query(`SELECT hour, kind, name, model, provider, tenant, runs::text AS runs, errors::text AS errors, cost,
99
+ input_tokens::text AS input_tokens, output_tokens::text AS output_tokens, latency::text AS latency
100
+ FROM ${this.table}
101
+ ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
102
+ ORDER BY hour, kind, name, model, provider, tenant`, params.values);
103
+ return rows.map((raw) => {
104
+ const row = raw;
105
+ return {
106
+ hour: String(row.hour),
107
+ kind: row.kind,
108
+ name: String(row.name),
109
+ model: String(row.model),
110
+ provider: String(row.provider),
111
+ tenant: String(row.tenant),
112
+ runs: Number(row.runs),
113
+ errors: Number(row.errors),
114
+ cost: Number(row.cost),
115
+ inputTokens: Number(row.input_tokens),
116
+ outputTokens: Number(row.output_tokens),
117
+ latency: String(row.latency).replace(/[{}]/g, '').split(',').filter(Boolean).map(Number),
118
+ };
119
+ });
120
+ }
121
+ }
122
+ function bucketsOf(latency) {
123
+ return Array.from({ length: ROLLUP_LATENCY_BOUNDS_MS.length + 1 }, (_, index) => Math.round(latency[index] ?? 0));
124
+ }
125
+ function hourOf(iso) {
126
+ const date = new Date(iso);
127
+ date.setUTCMinutes(0, 0, 0);
128
+ return date.toISOString();
129
+ }