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
@@ -2,9 +2,37 @@ export class MemoryCircuitStateStore {
2
2
  now;
3
3
  states = new Map();
4
4
  probes = new Map();
5
+ windows = new Map();
5
6
  constructor(now = () => Date.now()) {
6
7
  this.now = now;
7
8
  }
9
+ observe(providerName, observation) {
10
+ let window = this.windows.get(providerName);
11
+ if (!window) {
12
+ window = { buckets: new Map(), streak: 0 };
13
+ this.windows.set(providerName, window);
14
+ }
15
+ for (const bucket of observation.buckets) {
16
+ const stored = window.buckets.get(bucket.at) ?? { calls: 0, failures: 0 };
17
+ stored.calls += bucket.calls;
18
+ stored.failures += bucket.failures;
19
+ window.buckets.set(bucket.at, stored);
20
+ }
21
+ window.streak = observation.succeeded ? observation.trailingFailures : window.streak + observation.trailingFailures;
22
+ const keepAfter = observation.now - observation.windowMs - observation.bucketMs;
23
+ const since = observation.since ?? Number.NEGATIVE_INFINITY;
24
+ let calls = 0;
25
+ let failures = 0;
26
+ for (const [at, bucket] of window.buckets) {
27
+ if (at <= keepAfter)
28
+ window.buckets.delete(at);
29
+ else if (at >= since) {
30
+ calls += bucket.calls;
31
+ failures += bucket.failures;
32
+ }
33
+ }
34
+ return { calls, failures, consecutiveFailures: window.streak };
35
+ }
8
36
  read() {
9
37
  return [...this.states.values()].map((state) => ({ ...state }));
10
38
  }
@@ -34,14 +62,54 @@ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
34
62
  redis.call('DEL', KEYS[2])
35
63
  return 1
36
64
  `;
65
+ const OBSERVE_SCRIPT = `
66
+ local key = KEYS[1]
67
+ for i = 6, #ARGV, 3 do
68
+ redis.call('HINCRBY', key, 'c:' .. ARGV[i], ARGV[i + 1])
69
+ redis.call('HINCRBY', key, 'f:' .. ARGV[i], ARGV[i + 2])
70
+ end
71
+ local streak
72
+ if ARGV[1] == '1' then
73
+ redis.call('HSET', key, 'streak', ARGV[2])
74
+ streak = tonumber(ARGV[2])
75
+ else
76
+ streak = tonumber(redis.call('HINCRBY', key, 'streak', ARGV[2]))
77
+ end
78
+ local keepAfter = tonumber(ARGV[3])
79
+ local since = tonumber(ARGV[4])
80
+ local calls, failures = 0, 0
81
+ local fields = redis.call('HGETALL', key)
82
+ for i = 1, #fields, 2 do
83
+ local name = fields[i]
84
+ local kind = string.sub(name, 1, 2)
85
+ if kind == 'c:' or kind == 'f:' then
86
+ local at = tonumber(string.sub(name, 3))
87
+ if at <= keepAfter then
88
+ redis.call('HDEL', key, name)
89
+ elseif at >= since then
90
+ if kind == 'c:' then calls = calls + tonumber(fields[i + 1]) else failures = failures + tonumber(fields[i + 1]) end
91
+ end
92
+ end
93
+ end
94
+ redis.call('PEXPIRE', key, ARGV[5])
95
+ return { calls, failures, streak }
96
+ `;
37
97
  export class RedisCircuitStateStore {
38
98
  client;
39
99
  prefix;
40
100
  useEval;
101
+ observe;
41
102
  constructor(client, options = {}) {
42
103
  this.client = client;
43
104
  this.prefix = options.prefix ?? 'nexus-ai-pro:circuits:';
44
105
  this.useEval = options.useEval !== false && typeof client.eval === 'function';
106
+ if (this.useEval)
107
+ this.observe = (providerName, observation) => this.observeWithEval(providerName, observation);
108
+ }
109
+ async observeWithEval(providerName, observation) {
110
+ const reply = (await this.client.eval(OBSERVE_SCRIPT, 1, `${this.prefix}window:${providerName}`, observation.succeeded ? '1' : '0', String(observation.trailingFailures), String(observation.now - observation.windowMs - observation.bucketMs), String(observation.since ?? -1), String(observation.windowMs * 2 + observation.bucketMs), ...observation.buckets.flatMap((bucket) => [String(bucket.at), String(bucket.calls), String(bucket.failures)])));
111
+ const [calls, failures, streak] = (reply ?? []).map(Number);
112
+ return { calls: calls ?? 0, failures: failures ?? 0, consecutiveFailures: streak ?? 0 };
45
113
  }
46
114
  async read() {
47
115
  const all = (await this.client.hgetall(this.statesKey())) ?? {};
@@ -26,6 +26,8 @@ export declare class CircuitCoordinator {
26
26
  private lastSyncAt;
27
27
  /** False after a store failure, until a sync succeeds; the breaker then decides locally. */
28
28
  private reachable;
29
+ /** Calls not yet added to the shared window, per provider, with `shareObservations`. */
30
+ private readonly unreported;
29
31
  constructor(host: CircuitHost);
30
32
  /**
31
33
  * Whether this worker may go half-open once a cooldown has elapsed. With a reachable store only
@@ -35,6 +37,11 @@ export declare class CircuitCoordinator {
35
37
  /** Starts a background sync when the last one is older than the interval. Never waits. */
36
38
  tick(): void;
37
39
  sync(): Promise<void>;
40
+ /**
41
+ * Counts one call toward the shared window, to be added on the next sync. Cheap and synchronous:
42
+ * a check never waits for the store.
43
+ */
44
+ observed(providerName: string, failed: boolean): void;
38
45
  /** Publishes a transition this worker made. */
39
46
  published(providerName: string, entry: CircuitEntry, to: 'open' | 'closed', reason: string): void;
40
47
  /** Publishes an operator reset, so every worker closes the circuit rather than only this one. */
@@ -45,6 +52,14 @@ export declare class CircuitCoordinator {
45
52
  */
46
53
  reconcile(): void;
47
54
  flush(): Promise<void>;
55
+ /**
56
+ * Adds this worker's unreported calls to each provider's shared window, then opens a closed circuit
57
+ * whose shared count crossed a threshold. The batch is taken before the first await, so calls made
58
+ * while it is in flight wait for the next sync rather than being lost or counted twice.
59
+ */
60
+ private report;
61
+ /** Opens a closed circuit whose shared window crossed the breaker's thresholds. */
62
+ private judge;
48
63
  /** Takes a decision another worker published, when it is newer than what this worker knows. */
49
64
  private adopt;
50
65
  /**
@@ -55,3 +70,5 @@ export declare class CircuitCoordinator {
55
70
  private publish;
56
71
  private failed;
57
72
  }
73
+ /** The width of one shared-window bucket: a twelfth of the window, so a bucket rolling out moves the count by little. */
74
+ export declare function bucketWidth(windowMs: number): number;
@@ -4,6 +4,7 @@ export class CircuitCoordinator {
4
4
  syncing;
5
5
  lastSyncAt = Number.NEGATIVE_INFINITY;
6
6
  reachable = true;
7
+ unreported = new Map();
7
8
  constructor(host) {
8
9
  this.host = host;
9
10
  }
@@ -26,6 +27,8 @@ export class CircuitCoordinator {
26
27
  this.syncing = (async () => {
27
28
  try {
28
29
  this.lastSyncAt = this.host.now();
30
+ if (this.unreported.size > 0)
31
+ await this.report(store);
29
32
  for (const shared of await store.read())
30
33
  this.adopt(shared);
31
34
  const resetTimeoutMs = this.host.config.resetTimeoutMs ?? 30_000;
@@ -51,6 +54,29 @@ export class CircuitCoordinator {
51
54
  })();
52
55
  return this.syncing;
53
56
  }
57
+ observed(providerName, failed) {
58
+ if (!this.host.config.shareObservations || !this.host.config.store?.observe)
59
+ return;
60
+ const bucketMs = bucketWidth(this.host.config.windowMs ?? 60_000);
61
+ const at = Math.floor(this.host.now() / bucketMs) * bucketMs;
62
+ let pending = this.unreported.get(providerName);
63
+ if (!pending) {
64
+ pending = { buckets: new Map(), succeeded: false, trailing: 0 };
65
+ this.unreported.set(providerName, pending);
66
+ }
67
+ const bucket = pending.buckets.get(at) ?? { calls: 0, failures: 0 };
68
+ bucket.calls += 1;
69
+ if (failed)
70
+ bucket.failures += 1;
71
+ pending.buckets.set(at, bucket);
72
+ if (failed)
73
+ pending.trailing += 1;
74
+ else {
75
+ pending.succeeded = true;
76
+ pending.trailing = 0;
77
+ }
78
+ this.tick();
79
+ }
54
80
  published(providerName, entry, to, reason) {
55
81
  entry.changedAt = this.stampAfter(entry.changedAt);
56
82
  this.publish({
@@ -72,6 +98,23 @@ export class CircuitCoordinator {
72
98
  reconcile() {
73
99
  for (const name of [...this.host.names()]) {
74
100
  const entry = this.host.entry(name);
101
+ if (this.host.config.shareObservations && this.host.config.store?.observe && entry.window.length > 0) {
102
+ const bucketMs = bucketWidth(this.host.config.windowMs ?? 60_000);
103
+ const buckets = new Map();
104
+ for (const call of entry.window) {
105
+ const at = Math.floor(call.at / bucketMs) * bucketMs;
106
+ const bucket = buckets.get(at) ?? { calls: 0, failures: 0 };
107
+ bucket.calls += 1;
108
+ if (call.failed)
109
+ bucket.failures += 1;
110
+ buckets.set(at, bucket);
111
+ }
112
+ this.unreported.set(name, {
113
+ buckets,
114
+ succeeded: entry.window.some((call) => !call.failed),
115
+ trailing: entry.consecutiveFailures,
116
+ });
117
+ }
75
118
  if (entry.state === 'open' && entry.changedAt === Number.NEGATIVE_INFINITY) {
76
119
  this.published(name, entry, 'open', entry.reason ?? 'opened while shared state was loading');
77
120
  }
@@ -81,6 +124,46 @@ export class CircuitCoordinator {
81
124
  async flush() {
82
125
  await Promise.all([...this.pending]);
83
126
  }
127
+ async report(store) {
128
+ if (!store.observe || this.unreported.size === 0)
129
+ return;
130
+ const batches = [...this.unreported];
131
+ this.unreported.clear();
132
+ const windowMs = this.host.config.windowMs ?? 60_000;
133
+ for (const [name, pending] of batches) {
134
+ const entry = this.host.entry(name);
135
+ const window = await store.observe(name, {
136
+ buckets: [...pending.buckets].map(([at, bucket]) => ({ at, calls: bucket.calls, failures: bucket.failures })),
137
+ succeeded: pending.succeeded,
138
+ trailingFailures: pending.trailing,
139
+ windowMs,
140
+ bucketMs: bucketWidth(windowMs),
141
+ now: this.host.now(),
142
+ ...(entry.state === 'closed' && Number.isFinite(entry.changedAt) ? { since: entry.changedAt } : {}),
143
+ });
144
+ entry.shared = {
145
+ calls: window.calls,
146
+ failures: window.failures,
147
+ consecutiveFailures: window.consecutiveFailures,
148
+ };
149
+ this.judge(name, entry, window);
150
+ }
151
+ }
152
+ judge(name, entry, window) {
153
+ if (entry.state !== 'closed')
154
+ return;
155
+ const config = this.host.config;
156
+ if (window.consecutiveFailures >= (config.failureThreshold ?? 5)) {
157
+ this.host.transition(name, entry, 'open', 'shared failure threshold reached');
158
+ return;
159
+ }
160
+ const rate = config.failureRateThreshold;
161
+ if (rate !== undefined &&
162
+ window.calls >= (config.minimumThroughput ?? 10) &&
163
+ window.failures / window.calls >= rate) {
164
+ this.host.transition(name, entry, 'open', 'shared failure rate threshold reached');
165
+ }
166
+ }
84
167
  adopt(shared) {
85
168
  const entry = this.host.entry(shared.providerName);
86
169
  if (shared.updatedAt <= entry.changedAt)
@@ -120,3 +203,6 @@ export class CircuitCoordinator {
120
203
  }
121
204
  }
122
205
  }
206
+ export function bucketWidth(windowMs) {
207
+ return Math.max(1, Math.floor(windowMs / 12));
208
+ }
@@ -10,6 +10,8 @@ export interface RateLimitedRequest {
10
10
  model?: string;
11
11
  /** Caller, for per-user limits. */
12
12
  userId?: string;
13
+ /** Tenant, for per-tenant limits. */
14
+ tenantId?: string;
13
15
  }
14
16
  /** Raised when a caller exceeds its rate limit. */
15
17
  export declare class NexusRateLimitError extends Error {
@@ -63,6 +63,8 @@ export class RateLimiter {
63
63
  getKey(request, config) {
64
64
  if (config.key === 'model')
65
65
  return `model:${request.model}`;
66
+ if (config.key === 'tenantId')
67
+ return `tenant:${request.tenantId || 'none'}`;
66
68
  if (config.key === 'global')
67
69
  return 'global';
68
70
  return `user:${request.userId || 'anonymous'}`;
@@ -1,5 +1,7 @@
1
- import type { CircuitStateStore, SharedCircuitState } from '../ops/circuit-breaker.js';
1
+ import type { CircuitObservation, CircuitStateStore, CircuitWindow, SharedCircuitState } from '../ops/circuit-breaker.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 circuit store. */
4
6
  export interface PostgresCircuitStateStoreOptions {
5
7
  /** Table name, optionally schema-qualified. Defaults to `nexus_circuits`. */
@@ -7,8 +9,13 @@ export interface PostgresCircuitStateStoreOptions {
7
9
  /** Clock for probe leases. Defaults to `Date.now`, the same clock the breaker uses. */
8
10
  now?: () => number;
9
11
  }
10
- /** The schema, as statements. */
12
+ /** The schema, as statements: every migration's, in order. */
11
13
  export declare function circuitStoreMigration(options?: Pick<PostgresCircuitStateStoreOptions, 'table'>): string[];
14
+ /**
15
+ * The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 table;
16
+ * version 2 adds the shared failure window, so a breaker counts every worker's calls.
17
+ */
18
+ export declare function circuitStoreMigrations(options?: Pick<PostgresCircuitStateStoreOptions, 'table'>): SchemaMigration[];
12
19
  /**
13
20
  * Shared circuit state in Postgres.
14
21
  *
@@ -20,10 +27,14 @@ export declare class PostgresCircuitStateStore implements CircuitStateStore {
20
27
  private readonly client;
21
28
  private readonly options;
22
29
  private readonly table;
30
+ private readonly windowTable;
23
31
  private readonly now;
24
32
  constructor(client: PostgresLikeClient, options?: PostgresCircuitStateStoreOptions);
25
- /** Creates the table if it does not exist. Never runs implicitly. */
26
- migrate(): Promise<void>;
33
+ /**
34
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
35
+ * Never runs implicitly. Safe on a database an older release created: what exists is kept.
36
+ */
37
+ migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
27
38
  /** Every provider's shared circuit state. */
28
39
  read(): Promise<SharedCircuitState[]>;
29
40
  /**
@@ -35,4 +46,10 @@ export declare class PostgresCircuitStateStore implements CircuitStateStore {
35
46
  * claimant, including when it already holds the claim.
36
47
  */
37
48
  claimProbe(providerName: string, owner: string, ttlMs: number): Promise<boolean>;
49
+ /**
50
+ * Adds one worker's calls to a provider's shared window, then sums it. The add, the trim of buckets
51
+ * that rolled out, and the streak update are one statement, so two workers reporting at once both
52
+ * count. Needs version 2 of the schema.
53
+ */
54
+ observe(providerName: string, observation: CircuitObservation): Promise<CircuitWindow>;
38
55
  }
@@ -1,28 +1,58 @@
1
- import { fromJson, quoteTable, runStatements } from './client.js';
1
+ import { fromJson, quoteTable } from './client.js';
2
2
  export function circuitStoreMigration(options = {}) {
3
+ return circuitStoreMigrations(options).flatMap((migration) => [...migration.statements]);
4
+ }
5
+ export function circuitStoreMigrations(options = {}) {
6
+ const name = options.table ?? 'nexus_circuits';
7
+ const component = `circuits:${name}`;
3
8
  return [
4
- `CREATE TABLE IF NOT EXISTS ${quoteTable(options.table ?? 'nexus_circuits')} (
9
+ {
10
+ component,
11
+ version: 1,
12
+ name: 'create the circuits table',
13
+ statements: [
14
+ `CREATE TABLE IF NOT EXISTS ${quoteTable(name)} (
5
15
  provider text PRIMARY KEY,
6
16
  updated_at double precision,
7
17
  doc jsonb,
8
18
  probe_owner text,
9
19
  probe_expires_at double precision
10
20
  )`,
21
+ ],
22
+ },
23
+ {
24
+ component,
25
+ version: 2,
26
+ name: 'add the shared failure window',
27
+ statements: [
28
+ `ALTER TABLE ${quoteTable(name)} ADD COLUMN IF NOT EXISTS failure_streak integer NOT NULL DEFAULT 0`,
29
+ `CREATE TABLE IF NOT EXISTS ${quoteTable(`${name}_window`)} (
30
+ provider text NOT NULL,
31
+ bucket double precision NOT NULL,
32
+ calls integer NOT NULL,
33
+ failures integer NOT NULL,
34
+ PRIMARY KEY (provider, bucket)
35
+ )`,
36
+ ],
37
+ },
11
38
  ];
12
39
  }
13
40
  export class PostgresCircuitStateStore {
14
41
  client;
15
42
  options;
16
43
  table;
44
+ windowTable;
17
45
  now;
18
46
  constructor(client, options = {}) {
19
47
  this.client = client;
20
48
  this.options = options;
21
49
  this.table = quoteTable(options.table ?? 'nexus_circuits');
50
+ this.windowTable = quoteTable(`${options.table ?? 'nexus_circuits'}_window`);
22
51
  this.now = options.now ?? (() => Date.now());
23
52
  }
24
- async migrate() {
25
- await runStatements(this.client, circuitStoreMigration(this.options));
53
+ async migrate(options = {}) {
54
+ const { applyPostgresMigrations } = await import('./migrations.js');
55
+ return applyPostgresMigrations(this.client, circuitStoreMigrations(this.options), options);
26
56
  }
27
57
  async read() {
28
58
  const { rows } = await this.client.query(`SELECT doc::text AS doc FROM ${this.table} WHERE doc IS NOT NULL`);
@@ -43,4 +73,35 @@ export class PostgresCircuitStateStore {
43
73
  RETURNING provider`, [providerName, owner, now, now + ttlMs]);
44
74
  return rows.length > 0;
45
75
  }
76
+ async observe(providerName, observation) {
77
+ const keepAfter = observation.now - observation.windowMs - observation.bucketMs;
78
+ const { rows } = await this.client.query(`WITH added AS (
79
+ INSERT INTO ${this.windowTable} AS stored (provider, bucket, calls, failures)
80
+ SELECT $1, item.at, item.calls, item.failures
81
+ FROM jsonb_to_recordset($2::jsonb) AS item(at double precision, calls integer, failures integer)
82
+ ON CONFLICT (provider, bucket) DO UPDATE
83
+ SET calls = stored.calls + EXCLUDED.calls, failures = stored.failures + EXCLUDED.failures
84
+ ), trimmed AS (
85
+ DELETE FROM ${this.windowTable} WHERE provider = $1 AND bucket <= $5::double precision
86
+ )
87
+ INSERT INTO ${this.table} AS circuit (provider, failure_streak) VALUES ($1, $3::integer)
88
+ ON CONFLICT (provider) DO UPDATE SET failure_streak = CASE WHEN $4::integer = 1
89
+ THEN EXCLUDED.failure_streak ELSE circuit.failure_streak + EXCLUDED.failure_streak END
90
+ RETURNING failure_streak`, [
91
+ providerName,
92
+ JSON.stringify(observation.buckets),
93
+ observation.trailingFailures,
94
+ observation.succeeded ? 1 : 0,
95
+ keepAfter,
96
+ ]);
97
+ const totals = await this.client.query(`SELECT coalesce(sum(calls), 0)::int AS calls, coalesce(sum(failures), 0)::int AS failures
98
+ FROM ${this.windowTable}
99
+ WHERE provider = $1 AND bucket > $2::double precision AND bucket >= $3::double precision`, [providerName, keepAfter, observation.since ?? -1]);
100
+ const sums = totals.rows[0];
101
+ return {
102
+ calls: Number(sums?.calls ?? 0),
103
+ failures: Number(sums?.failures ?? 0),
104
+ consecutiveFailures: Number(rows[0]?.failure_streak ?? 0),
105
+ };
106
+ }
46
107
  }
@@ -37,7 +37,5 @@ export declare function quoteDerived(table: string, suffix: string): string;
37
37
  * reading text everywhere is the only way to get the same value from every driver.
38
38
  */
39
39
  export declare function fromJson<T>(value: unknown): T;
40
- /** Runs migration statements one at a time, which every driver accepts. */
41
- export declare function runStatements(client: PostgresLikeClient, statements: readonly string[]): Promise<void>;
42
40
  /** Whether an error is Postgres refusing a duplicate key. */
43
41
  export declare function isUniqueViolation(error: unknown): boolean;
@@ -22,10 +22,6 @@ export function quoteDerived(table, suffix) {
22
22
  export function fromJson(value) {
23
23
  return (typeof value === 'string' ? JSON.parse(value) : value);
24
24
  }
25
- export async function runStatements(client, statements) {
26
- for (const statement of statements)
27
- await client.query(statement);
28
- }
29
25
  export function isUniqueViolation(error) {
30
26
  return error?.code === '23505';
31
27
  }
@@ -1,5 +1,7 @@
1
1
  import type { Dataset, DatasetStore, Experiment, ExperimentStore } from '../types/evaluate.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 evaluation stores. */
4
6
  export interface PostgresEvaluationStoreOptions {
5
7
  /** Datasets table. Defaults to `nexus_datasets`. */
@@ -7,6 +9,10 @@ export interface PostgresEvaluationStoreOptions {
7
9
  /** Experiments table. Defaults to `nexus_experiments`. */
8
10
  experimentsTable?: string;
9
11
  }
12
+ /**
13
+ * The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 schema.
14
+ */
15
+ export declare function evaluationStoreMigrations(options?: PostgresEvaluationStoreOptions): SchemaMigration[];
10
16
  /** The schema for both tables, as statements. */
11
17
  export declare function evaluationStoreMigration(options?: PostgresEvaluationStoreOptions): string[];
12
18
  /**
@@ -20,8 +26,11 @@ export declare class PostgresDatasetStore implements DatasetStore {
20
26
  private readonly options;
21
27
  private readonly table;
22
28
  constructor(client: PostgresLikeClient, options?: PostgresEvaluationStoreOptions);
23
- /** Creates both evaluation 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 dataset version, replacing one with the same name and version. */
26
35
  save(dataset: Dataset): Promise<void>;
27
36
  /** Returns a version, or the newest when none is given. */
@@ -38,8 +47,11 @@ export declare class PostgresExperimentStore implements ExperimentStore {
38
47
  private readonly options;
39
48
  private readonly table;
40
49
  constructor(client: PostgresLikeClient, options?: PostgresEvaluationStoreOptions);
41
- /** Creates both evaluation tables if they do not exist. Never runs implicitly. */
42
- migrate(): Promise<void>;
50
+ /**
51
+ * Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
52
+ * Never runs implicitly. Safe on a database an older release created: what exists is kept.
53
+ */
54
+ migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
43
55
  /** Stores an experiment, replacing one with the same id. */
44
56
  save(experiment: Experiment): Promise<void>;
45
57
  /** Reads an experiment by id. */
@@ -1,4 +1,14 @@
1
- import { fromJson, quoteDerived, quoteTable, runStatements } from './client.js';
1
+ import { fromJson, quoteDerived, quoteTable } from './client.js';
2
+ export function evaluationStoreMigrations(options = {}) {
3
+ return [
4
+ {
5
+ component: `evaluation:${options.datasetsTable ?? 'nexus_datasets'},${options.experimentsTable ?? 'nexus_experiments'}`,
6
+ version: 1,
7
+ name: 'create the dataset and experiment tables',
8
+ statements: evaluationStoreMigration(options),
9
+ },
10
+ ];
11
+ }
2
12
  export function evaluationStoreMigration(options = {}) {
3
13
  const datasetsName = options.datasetsTable ?? 'nexus_datasets';
4
14
  const experimentsName = options.experimentsTable ?? 'nexus_experiments';
@@ -33,8 +43,9 @@ export class PostgresDatasetStore {
33
43
  this.options = options;
34
44
  this.table = quoteTable(options.datasetsTable ?? 'nexus_datasets');
35
45
  }
36
- async migrate() {
37
- await runStatements(this.client, evaluationStoreMigration(this.options));
46
+ async migrate(options = {}) {
47
+ const { applyPostgresMigrations } = await import('./migrations.js');
48
+ return applyPostgresMigrations(this.client, evaluationStoreMigrations(this.options), options);
38
49
  }
39
50
  async save(dataset) {
40
51
  await this.client.query(`INSERT INTO ${this.table} (name, version, created_at, doc) VALUES ($1, $2, $3, $4::jsonb)
@@ -67,8 +78,9 @@ export class PostgresExperimentStore {
67
78
  this.options = options;
68
79
  this.table = quoteTable(options.experimentsTable ?? 'nexus_experiments');
69
80
  }
70
- async migrate() {
71
- await runStatements(this.client, evaluationStoreMigration(this.options));
81
+ async migrate(options = {}) {
82
+ const { applyPostgresMigrations } = await import('./migrations.js');
83
+ return applyPostgresMigrations(this.client, evaluationStoreMigrations(this.options), options);
72
84
  }
73
85
  async save(experiment) {
74
86
  await this.client.query(`INSERT INTO ${this.table} (id, name, dataset_name, dataset_version, started_at, doc)
@@ -1,14 +1,18 @@
1
- export { circuitStoreMigration, PostgresCircuitStateStore, type PostgresCircuitStateStoreOptions, } from './circuits.js';
1
+ import { type SchemaMigration } from '../utils/schema-migrations.js';
2
+ export { type AppliedMigration, type MigrationResult, type MigrationStatus, migrationChecksum, type SchemaMigration, SchemaMigrationError, } from '../utils/schema-migrations.js';
3
+ export { circuitStoreMigration, circuitStoreMigrations, PostgresCircuitStateStore, type PostgresCircuitStateStoreOptions, } from './circuits.js';
2
4
  export { fromPostgresJs, type PostgresJsLike, type PostgresLikeClient } from './client.js';
3
- export { evaluationStoreMigration, PostgresDatasetStore, type PostgresEvaluationStoreOptions, PostgresExperimentStore, } from './evaluate.js';
4
- export { operationStoreMigration, PostgresOperationStore, type PostgresOperationStoreOptions } from './operations.js';
5
- export { PostgresPromptStore, type PostgresPromptStoreOptions, promptStoreMigration } from './prompts.js';
6
- export { PostgresStore, type PostgresStoreOptions, storeMigration } from './store.js';
7
- export { PostgresTraceStore, type PostgresTraceStoreOptions, traceStoreMigration } from './traces.js';
8
- export { PostgresVectorStore, type PostgresVectorStoreOptions, vectorStoreMigration } from './vectors.js';
5
+ export { evaluationStoreMigration, evaluationStoreMigrations, PostgresDatasetStore, type PostgresEvaluationStoreOptions, PostgresExperimentStore, } from './evaluate.js';
6
+ export { applyPostgresMigrations, type PostgresMigrateOptions, postgresMigrationStatus, } from './migrations.js';
7
+ export { operationStoreMigration, operationStoreMigrations, PostgresOperationStore, type PostgresOperationStoreOptions, } from './operations.js';
8
+ export { PostgresPromptStore, type PostgresPromptStoreOptions, promptStoreMigration, promptStoreMigrations, } from './prompts.js';
9
+ export { PostgresRollupStore, type PostgresRollupStoreOptions, rollupStoreMigration, rollupStoreMigrations, } from './rollups.js';
10
+ export { PostgresStore, type PostgresStoreOptions, storeMigration, storeMigrations } from './store.js';
11
+ export { PostgresTraceStore, type PostgresTraceStoreOptions, traceStoreMigration, traceStoreMigrations, } from './traces.js';
12
+ export { PostgresVectorStore, type PostgresVectorStoreOptions, vectorStoreMigration, vectorStoreMigrations, } from './vectors.js';
9
13
  /** The Postgres adapters a migration can include. */
10
- export type PostgresAdapter = 'operations' | 'store' | 'traces' | 'evaluation' | 'circuits' | 'prompts' | 'vectors';
11
- /** Options for `postgresMigration()`. */
14
+ export type PostgresAdapter = 'operations' | 'store' | 'traces' | 'evaluation' | 'circuits' | 'prompts' | 'rollups' | 'vectors';
15
+ /** Options for `postgresMigrations()` and `postgresMigration()`. */
12
16
  export interface PostgresMigrationOptions {
13
17
  /**
14
18
  * Adapters to include. Defaults to every one but `vectors`, which needs pgvector and
@@ -18,10 +22,27 @@ export interface PostgresMigrationOptions {
18
22
  /** Enables pgvector for the long-term store, and sets the width of the `vectors` table. */
19
23
  vectorDimensions?: number;
20
24
  }
25
+ /** Options for `postgresMigration()`. */
26
+ export interface PostgresMigrationScriptOptions extends PostgresMigrationOptions {
27
+ /**
28
+ * Also creates `nexus_schema_migrations` and records every migration in the script as applied, so
29
+ * `nexus db status` agrees with a database built by the application's own tooling. Off by default.
30
+ */
31
+ record?: boolean;
32
+ }
33
+ /** Every adapter, in the order a migration applies them. */
34
+ export declare const POSTGRES_ADAPTERS: readonly PostgresAdapter[];
35
+ /**
36
+ * The versioned migrations of the chosen adapters on their default tables, for
37
+ * `applyPostgresMigrations()`, `postgresMigrationStatus()`, and `nexus db migrate`. A store on a
38
+ * custom table migrates through its own `migrate()`.
39
+ */
40
+ export declare function postgresMigrations(options?: PostgresMigrationOptions): SchemaMigration[];
21
41
  /**
22
42
  * The schema for the chosen adapters, as one SQL script.
23
43
  *
24
44
  * For migration tooling the application already has — Flyway, Prisma, a plain `psql -f` — which is
25
- * usually where a schema change belongs. `nexus db sql` prints the same script.
45
+ * usually where a schema change belongs. Every statement is idempotent, so the script runs again
46
+ * safely after an upgrade. `nexus db sql` prints the same script.
26
47
  */
27
- export declare function postgresMigration(options?: PostgresMigrationOptions): string;
48
+ export declare function postgresMigration(options?: PostgresMigrationScriptOptions): string;