nexus-ai-pro 2.0.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 (282) hide show
  1. package/API_STABILITY.md +65 -0
  2. package/CHANGELOG.md +267 -1
  3. package/MIGRATING.md +1 -1
  4. package/README.md +17 -8
  5. package/dist/agent/create-agent.d.ts +9 -1
  6. package/dist/agent/create-agent.js +40 -1
  7. package/dist/cache/memory-cache.d.ts +8 -0
  8. package/dist/cache/memory-cache.js +9 -0
  9. package/dist/cache/semantic-cache.d.ts +6 -3
  10. package/dist/cache/semantic-cache.js +5 -2
  11. package/dist/cli/args.js +3 -0
  12. package/dist/cli/db.d.ts +16 -4
  13. package/dist/cli/db.js +169 -22
  14. package/dist/cli/doctor.d.ts +17 -0
  15. package/dist/cli/doctor.js +53 -0
  16. package/dist/cli/graph.d.ts +9 -0
  17. package/dist/cli/graph.js +47 -0
  18. package/dist/cli.js +21 -2
  19. package/dist/core/lifecycle.js +11 -6
  20. package/dist/core/nexus.d.ts +12 -2
  21. package/dist/core/nexus.js +106 -23
  22. package/dist/doctor/index.d.ts +90 -0
  23. package/dist/doctor/index.js +237 -0
  24. package/dist/evals/judge.d.ts +11 -0
  25. package/dist/evals/judge.js +33 -0
  26. package/dist/evaluate/index.d.ts +1 -0
  27. package/dist/evaluate/index.js +1 -0
  28. package/dist/evaluate/provenance.d.ts +22 -0
  29. package/dist/evaluate/provenance.js +10 -0
  30. package/dist/evaluate/run.js +20 -4
  31. package/dist/graph/durability.d.ts +27 -0
  32. package/dist/graph/durability.js +66 -0
  33. package/dist/graph/errors.d.ts +30 -4
  34. package/dist/graph/errors.js +18 -2
  35. package/dist/graph/event-stream.d.ts +75 -0
  36. package/dist/graph/event-stream.js +289 -0
  37. package/dist/graph/functional.d.ts +80 -7
  38. package/dist/graph/functional.js +138 -35
  39. package/dist/graph/graph.d.ts +56 -8
  40. package/dist/graph/graph.js +331 -49
  41. package/dist/graph/index.d.ts +4 -2
  42. package/dist/graph/index.js +3 -1
  43. package/dist/graph/lint.d.ts +57 -0
  44. package/dist/graph/lint.js +141 -0
  45. package/dist/graph/run-control.d.ts +28 -0
  46. package/dist/graph/run-control.js +26 -0
  47. package/dist/images/evals.js +4 -2
  48. package/dist/operations/adapters.d.ts +52 -4
  49. package/dist/operations/adapters.js +194 -7
  50. package/dist/operations/runner.js +14 -15
  51. package/dist/operations/stats.js +8 -1
  52. package/dist/operations/store.d.ts +5 -1
  53. package/dist/operations/store.js +7 -0
  54. package/dist/ops/circuit-breaker.d.ts +52 -0
  55. package/dist/ops/circuit-breaker.js +4 -0
  56. package/dist/ops/circuit-store.d.ts +13 -2
  57. package/dist/ops/circuit-store.js +68 -0
  58. package/dist/ops/circuit-sync.d.ts +17 -0
  59. package/dist/ops/circuit-sync.js +86 -0
  60. package/dist/ops/health.d.ts +24 -0
  61. package/dist/ops/health.js +38 -10
  62. package/dist/ops/index.d.ts +1 -1
  63. package/dist/ops/index.js +1 -1
  64. package/dist/ops/rate-limit-adapters.d.ts +23 -0
  65. package/dist/ops/rate-limit-adapters.js +34 -0
  66. package/dist/ops/rate-limiter.d.ts +14 -2
  67. package/dist/ops/rate-limiter.js +31 -0
  68. package/dist/postgres/circuits.d.ts +21 -4
  69. package/dist/postgres/circuits.js +65 -4
  70. package/dist/postgres/client.d.ts +0 -2
  71. package/dist/postgres/client.js +0 -4
  72. package/dist/postgres/evaluate.d.ts +16 -4
  73. package/dist/postgres/evaluate.js +17 -5
  74. package/dist/postgres/index.d.ts +32 -11
  75. package/dist/postgres/index.js +65 -31
  76. package/dist/postgres/migrations.d.ts +60 -0
  77. package/dist/postgres/migrations.js +121 -0
  78. package/dist/postgres/operations.d.ts +14 -3
  79. package/dist/postgres/operations.js +45 -15
  80. package/dist/postgres/prompts.d.ts +11 -2
  81. package/dist/postgres/prompts.js +14 -3
  82. package/dist/postgres/rollups.d.ts +35 -0
  83. package/dist/postgres/rollups.js +129 -0
  84. package/dist/postgres/store.d.ts +11 -2
  85. package/dist/postgres/store.js +14 -3
  86. package/dist/postgres/traces.d.ts +13 -3
  87. package/dist/postgres/traces.js +25 -3
  88. package/dist/postgres/vectors.d.ts +11 -2
  89. package/dist/postgres/vectors.js +14 -3
  90. package/dist/router/auto-router.d.ts +34 -7
  91. package/dist/router/auto-router.js +201 -68
  92. package/dist/server/assistant.d.ts +44 -0
  93. package/dist/server/assistant.js +39 -0
  94. package/dist/server/errors.d.ts +12 -0
  95. package/dist/server/errors.js +7 -0
  96. package/dist/server/index.d.ts +1 -1
  97. package/dist/server/index.js +1 -1
  98. package/dist/server/runs.d.ts +28 -1
  99. package/dist/server/runs.js +110 -14
  100. package/dist/sqlite/index.d.ts +16 -3
  101. package/dist/sqlite/index.js +16 -3
  102. package/dist/sqlite/migrations.d.ts +26 -0
  103. package/dist/sqlite/migrations.js +63 -0
  104. package/dist/sqlite/operations.d.ts +14 -3
  105. package/dist/sqlite/operations.js +42 -15
  106. package/dist/sqlite/store.d.ts +9 -2
  107. package/dist/sqlite/store.js +13 -3
  108. package/dist/sqlite/vectors.d.ts +11 -2
  109. package/dist/sqlite/vectors.js +13 -2
  110. package/dist/store/tenant.d.ts +12 -0
  111. package/dist/store/tenant.js +29 -0
  112. package/dist/tenancy/index.d.ts +113 -0
  113. package/dist/tenancy/index.js +233 -0
  114. package/dist/testing/record.d.ts +42 -0
  115. package/dist/testing/record.js +57 -5
  116. package/dist/tracing/index.d.ts +3 -3
  117. package/dist/tracing/index.js +2 -2
  118. package/dist/tracing/instrument.js +30 -0
  119. package/dist/tracing/otlp.d.ts +91 -0
  120. package/dist/tracing/otlp.js +124 -0
  121. package/dist/tracing/rollups.d.ts +100 -0
  122. package/dist/tracing/rollups.js +139 -0
  123. package/dist/tracing/stores.d.ts +17 -0
  124. package/dist/tracing/stores.js +23 -0
  125. package/dist/tracing/tracer.d.ts +50 -4
  126. package/dist/tracing/tracer.js +85 -6
  127. package/dist/types/config.d.ts +33 -2
  128. package/dist/types/evaluate.d.ts +37 -0
  129. package/dist/types/graph.d.ts +354 -9
  130. package/dist/types/graph.js +3 -1
  131. package/dist/types/messages.d.ts +5 -0
  132. package/dist/types/operations.d.ts +22 -2
  133. package/dist/types/response.d.ts +20 -0
  134. package/dist/types/server.d.ts +18 -0
  135. package/dist/types/tracing.d.ts +11 -0
  136. package/dist/utils/schema-migrations.d.ts +97 -0
  137. package/dist/utils/schema-migrations.js +91 -0
  138. package/dist/utils/signals.d.ts +17 -0
  139. package/dist/utils/signals.js +26 -0
  140. package/dist/utils/tenant.d.ts +14 -0
  141. package/dist/utils/tenant.js +16 -0
  142. package/dist/version.d.ts +5 -0
  143. package/dist/version.js +1 -0
  144. package/dist-cjs/agent/create-agent.d.ts +9 -1
  145. package/dist-cjs/agent/create-agent.js +40 -1
  146. package/dist-cjs/cache/memory-cache.d.ts +8 -0
  147. package/dist-cjs/cache/memory-cache.js +9 -0
  148. package/dist-cjs/cache/semantic-cache.d.ts +6 -3
  149. package/dist-cjs/cache/semantic-cache.js +5 -2
  150. package/dist-cjs/cli/args.js +3 -0
  151. package/dist-cjs/cli/db.d.ts +16 -4
  152. package/dist-cjs/cli/db.js +200 -20
  153. package/dist-cjs/cli/doctor.d.ts +17 -0
  154. package/dist-cjs/cli/doctor.js +56 -0
  155. package/dist-cjs/cli/graph.d.ts +9 -0
  156. package/dist-cjs/cli/graph.js +50 -0
  157. package/dist-cjs/core/lifecycle.js +11 -6
  158. package/dist-cjs/core/nexus.d.ts +12 -2
  159. package/dist-cjs/core/nexus.js +106 -23
  160. package/dist-cjs/doctor/index.d.ts +90 -0
  161. package/dist-cjs/doctor/index.js +276 -0
  162. package/dist-cjs/evals/judge.d.ts +11 -0
  163. package/dist-cjs/evals/judge.js +33 -0
  164. package/dist-cjs/evaluate/index.d.ts +1 -0
  165. package/dist-cjs/evaluate/index.js +3 -1
  166. package/dist-cjs/evaluate/provenance.d.ts +22 -0
  167. package/dist-cjs/evaluate/provenance.js +14 -0
  168. package/dist-cjs/evaluate/run.js +20 -4
  169. package/dist-cjs/graph/durability.d.ts +27 -0
  170. package/dist-cjs/graph/durability.js +70 -0
  171. package/dist-cjs/graph/errors.d.ts +30 -4
  172. package/dist-cjs/graph/errors.js +20 -3
  173. package/dist-cjs/graph/event-stream.d.ts +75 -0
  174. package/dist-cjs/graph/event-stream.js +295 -0
  175. package/dist-cjs/graph/functional.d.ts +80 -7
  176. package/dist-cjs/graph/functional.js +137 -34
  177. package/dist-cjs/graph/graph.d.ts +56 -8
  178. package/dist-cjs/graph/graph.js +330 -48
  179. package/dist-cjs/graph/index.d.ts +4 -2
  180. package/dist-cjs/graph/index.js +7 -1
  181. package/dist-cjs/graph/lint.d.ts +57 -0
  182. package/dist-cjs/graph/lint.js +144 -0
  183. package/dist-cjs/graph/run-control.d.ts +28 -0
  184. package/dist-cjs/graph/run-control.js +30 -0
  185. package/dist-cjs/images/evals.js +4 -2
  186. package/dist-cjs/operations/adapters.d.ts +52 -4
  187. package/dist-cjs/operations/adapters.js +193 -6
  188. package/dist-cjs/operations/runner.js +14 -15
  189. package/dist-cjs/operations/stats.js +8 -1
  190. package/dist-cjs/operations/store.d.ts +5 -1
  191. package/dist-cjs/operations/store.js +7 -0
  192. package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
  193. package/dist-cjs/ops/circuit-breaker.js +4 -0
  194. package/dist-cjs/ops/circuit-store.d.ts +13 -2
  195. package/dist-cjs/ops/circuit-store.js +68 -0
  196. package/dist-cjs/ops/circuit-sync.d.ts +17 -0
  197. package/dist-cjs/ops/circuit-sync.js +87 -0
  198. package/dist-cjs/ops/health.d.ts +24 -0
  199. package/dist-cjs/ops/health.js +38 -10
  200. package/dist-cjs/ops/index.d.ts +1 -1
  201. package/dist-cjs/ops/rate-limit-adapters.d.ts +23 -0
  202. package/dist-cjs/ops/rate-limit-adapters.js +35 -1
  203. package/dist-cjs/ops/rate-limiter.d.ts +14 -2
  204. package/dist-cjs/ops/rate-limiter.js +32 -0
  205. package/dist-cjs/postgres/circuits.d.ts +21 -4
  206. package/dist-cjs/postgres/circuits.js +98 -3
  207. package/dist-cjs/postgres/client.d.ts +0 -2
  208. package/dist-cjs/postgres/client.js +0 -5
  209. package/dist-cjs/postgres/evaluate.d.ts +16 -4
  210. package/dist-cjs/postgres/evaluate.js +50 -4
  211. package/dist-cjs/postgres/index.d.ts +32 -11
  212. package/dist-cjs/postgres/index.js +69 -20
  213. package/dist-cjs/postgres/migrations.d.ts +60 -0
  214. package/dist-cjs/postgres/migrations.js +129 -0
  215. package/dist-cjs/postgres/operations.d.ts +14 -3
  216. package/dist-cjs/postgres/operations.js +78 -14
  217. package/dist-cjs/postgres/prompts.d.ts +11 -2
  218. package/dist-cjs/postgres/prompts.js +47 -2
  219. package/dist-cjs/postgres/rollups.d.ts +35 -0
  220. package/dist-cjs/postgres/rollups.js +168 -0
  221. package/dist-cjs/postgres/store.d.ts +11 -2
  222. package/dist-cjs/postgres/store.js +47 -2
  223. package/dist-cjs/postgres/traces.d.ts +13 -3
  224. package/dist-cjs/postgres/traces.js +58 -2
  225. package/dist-cjs/postgres/vectors.d.ts +11 -2
  226. package/dist-cjs/postgres/vectors.js +47 -2
  227. package/dist-cjs/router/auto-router.d.ts +34 -7
  228. package/dist-cjs/router/auto-router.js +201 -68
  229. package/dist-cjs/server/assistant.d.ts +44 -0
  230. package/dist-cjs/server/assistant.js +39 -0
  231. package/dist-cjs/server/errors.d.ts +12 -0
  232. package/dist-cjs/server/errors.js +9 -1
  233. package/dist-cjs/server/index.d.ts +1 -1
  234. package/dist-cjs/server/index.js +2 -1
  235. package/dist-cjs/server/runs.d.ts +28 -1
  236. package/dist-cjs/server/runs.js +109 -13
  237. package/dist-cjs/sqlite/index.d.ts +16 -3
  238. package/dist-cjs/sqlite/index.js +31 -10
  239. package/dist-cjs/sqlite/migrations.d.ts +26 -0
  240. package/dist-cjs/sqlite/migrations.js +71 -0
  241. package/dist-cjs/sqlite/operations.d.ts +14 -3
  242. package/dist-cjs/sqlite/operations.js +76 -15
  243. package/dist-cjs/sqlite/store.d.ts +9 -2
  244. package/dist-cjs/sqlite/store.js +47 -3
  245. package/dist-cjs/sqlite/vectors.d.ts +11 -2
  246. package/dist-cjs/sqlite/vectors.js +47 -2
  247. package/dist-cjs/store/tenant.d.ts +12 -0
  248. package/dist-cjs/store/tenant.js +33 -0
  249. package/dist-cjs/tenancy/index.d.ts +113 -0
  250. package/dist-cjs/tenancy/index.js +250 -0
  251. package/dist-cjs/testing/record.d.ts +42 -0
  252. package/dist-cjs/testing/record.js +57 -5
  253. package/dist-cjs/tracing/index.d.ts +3 -3
  254. package/dist-cjs/tracing/index.js +4 -1
  255. package/dist-cjs/tracing/instrument.js +30 -0
  256. package/dist-cjs/tracing/otlp.d.ts +91 -0
  257. package/dist-cjs/tracing/otlp.js +129 -0
  258. package/dist-cjs/tracing/rollups.d.ts +100 -0
  259. package/dist-cjs/tracing/rollups.js +147 -0
  260. package/dist-cjs/tracing/stores.d.ts +17 -0
  261. package/dist-cjs/tracing/stores.js +24 -0
  262. package/dist-cjs/tracing/tracer.d.ts +50 -4
  263. package/dist-cjs/tracing/tracer.js +87 -6
  264. package/dist-cjs/types/config.d.ts +33 -2
  265. package/dist-cjs/types/evaluate.d.ts +37 -0
  266. package/dist-cjs/types/graph.d.ts +354 -9
  267. package/dist-cjs/types/graph.js +3 -1
  268. package/dist-cjs/types/messages.d.ts +5 -0
  269. package/dist-cjs/types/operations.d.ts +22 -2
  270. package/dist-cjs/types/response.d.ts +20 -0
  271. package/dist-cjs/types/server.d.ts +18 -0
  272. package/dist-cjs/types/tracing.d.ts +11 -0
  273. package/dist-cjs/utils/schema-migrations.d.ts +97 -0
  274. package/dist-cjs/utils/schema-migrations.js +102 -0
  275. package/dist-cjs/utils/signals.d.ts +17 -0
  276. package/dist-cjs/utils/signals.js +29 -0
  277. package/dist-cjs/utils/tenant.d.ts +14 -0
  278. package/dist-cjs/utils/tenant.js +22 -0
  279. package/dist-cjs/version.d.ts +5 -0
  280. package/dist-cjs/version.js +4 -0
  281. package/llms.txt +4 -3
  282. package/package.json +110 -2
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export { AuditLogger } from './audit-logger.js';
8
8
  export { type FamilyCallDescriptor, type FamilyRuntime, FamilyTelemetry } from './family-telemetry.js';
9
- export { type HealthConfig, ProviderHealthMonitor, type ProviderHealthSnapshot } from './health.js';
9
+ export { type HealthConfig, ProviderHealthMonitor, type ProviderHealthSnapshot, type ProviderHealthStatus, } from './health.js';
10
10
  export { InMemoryMetrics, type MetricsConfig, MetricsCollector, type MetricsSink, type OpenTelemetryLikeMeter, OpenTelemetryMetricsSink, } from './metrics.js';
11
11
  export { type OpenTelemetryLikeSpan, type OpenTelemetryLikeTracer, OpenTelemetryTraceExporter, } from './otel-tracing.js';
12
12
  export { NexusRateLimitError, type RateLimitedRequest, RateLimiter } from './rate-limiter.js';
package/dist/ops/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { AuditLogger } from './audit-logger.js';
2
2
  export { FamilyTelemetry } from './family-telemetry.js';
3
- export { ProviderHealthMonitor } from './health.js';
3
+ export { ProviderHealthMonitor, } from './health.js';
4
4
  export { InMemoryMetrics, MetricsCollector, OpenTelemetryMetricsSink, } from './metrics.js';
5
5
  export { OpenTelemetryTraceExporter, } from './otel-tracing.js';
6
6
  export { NexusRateLimitError, RateLimiter } from './rate-limiter.js';
@@ -1,3 +1,5 @@
1
+ import { gcraDecide } from './rate-limiter.js';
2
+ export { gcraDecide };
1
3
  /** A rate-limit window after counting one call. */
2
4
  export interface RateLimitHit {
3
5
  /** Calls counted in the current window, including this one. */
@@ -5,6 +7,13 @@ export interface RateLimitHit {
5
7
  /** Epoch milliseconds when the window resets. */
6
8
  resetAt: number;
7
9
  }
10
+ /** The verdict on one call under GCRA. */
11
+ export interface RateLimitDecision {
12
+ /** Whether the call may go. */
13
+ allowed: boolean;
14
+ /** Epoch milliseconds from which a refused call would be allowed. */
15
+ retryAt: number;
16
+ }
8
17
  /**
9
18
  * Where rate-limit counters live.
10
19
  *
@@ -16,14 +25,22 @@ export interface RateLimitStore {
16
25
  hit(key: string, windowMs: number): Promise<RateLimitHit> | RateLimitHit;
17
26
  /** Resets a key's window. */
18
27
  reset?(key: string): Promise<void> | void;
28
+ /**
29
+ * Decides one call under GCRA: one call per `emissionMs` on average, with up to `burst` at once,
30
+ * atomically. Needed for `algorithm: 'gcra'`; both bundled stores have it.
31
+ */
32
+ gcra?(key: string, emissionMs: number, burst: number): Promise<RateLimitDecision> | RateLimitDecision;
19
33
  }
20
34
  /** Process-local counters. The default, and equivalent to the limiter's built-in behavior. */
21
35
  export declare class MemoryRateLimitStore implements RateLimitStore {
22
36
  private readonly now;
23
37
  private readonly buckets;
38
+ private readonly arrivals;
24
39
  constructor(now?: () => number);
25
40
  /** Counts one call, starting a new window when the last has ended. */
26
41
  hit(key: string, windowMs: number): RateLimitHit;
42
+ /** Decides one call under GCRA. */
43
+ gcra(key: string, emissionMs: number, burst: number): RateLimitDecision;
27
44
  /** Resets a key's window. */
28
45
  reset(key: string): void;
29
46
  /** Resets every window. */
@@ -65,11 +82,17 @@ export interface RedisRateLimitStoreOptions {
65
82
  */
66
83
  export declare class RedisRateLimitStore implements RateLimitStore {
67
84
  private readonly client;
85
+ private readonly options;
68
86
  private readonly prefix;
69
87
  private readonly useEval;
70
88
  constructor(client: RedisRateLimitLikeClient, options?: RedisRateLimitStoreOptions);
71
89
  /** Counts one call, in one atomic step when the client has `eval`. */
72
90
  hit(key: string, windowMs: number): Promise<RateLimitHit>;
91
+ /**
92
+ * Decides one call under GCRA, atomically, on Redis's own clock. Needs a client with `eval`: the
93
+ * read and the write cannot be split without letting two workers both take the last slot.
94
+ */
95
+ gcra(key: string, emissionMs: number, burst: number): Promise<RateLimitDecision>;
73
96
  /** Resets a key's window. Does nothing when the client has no `del`. */
74
97
  reset(key: string): Promise<void>;
75
98
  }
@@ -1,6 +1,9 @@
1
+ import { gcraDecide } from './rate-limiter.js';
2
+ export { gcraDecide };
1
3
  export class MemoryRateLimitStore {
2
4
  now;
3
5
  buckets = new Map();
6
+ arrivals = new Map();
4
7
  constructor(now = () => Date.now()) {
5
8
  this.now = now;
6
9
  }
@@ -15,11 +18,19 @@ export class MemoryRateLimitStore {
15
18
  bucket.count += 1;
16
19
  return bucket;
17
20
  }
21
+ gcra(key, emissionMs, burst) {
22
+ const decided = gcraDecide(this.arrivals.get(key), this.now(), emissionMs, burst);
23
+ if (decided.allowed)
24
+ this.arrivals.set(key, decided.tat);
25
+ return { allowed: decided.allowed, retryAt: decided.retryAt };
26
+ }
18
27
  reset(key) {
19
28
  this.buckets.delete(key);
29
+ this.arrivals.delete(key);
20
30
  }
21
31
  clear() {
22
32
  this.buckets.clear();
33
+ this.arrivals.clear();
23
34
  }
24
35
  }
25
36
  const HIT_SCRIPT = `
@@ -35,12 +46,27 @@ if ttl < 0 then
35
46
  end
36
47
  return {count, ttl}
37
48
  `;
49
+ const GCRA_SCRIPT = `
50
+ local time = redis.call('TIME')
51
+ local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
52
+ local emission = tonumber(ARGV[1])
53
+ local burst = tonumber(ARGV[2])
54
+ local tat = tonumber(redis.call('GET', KEYS[1]) or now)
55
+ if tat < now then tat = now end
56
+ local next = tat + emission
57
+ local allowAt = next - burst * emission
58
+ if allowAt > now then return {0, allowAt - now} end
59
+ redis.call('SET', KEYS[1], next, 'PX', math.max(1, math.ceil(next - now)))
60
+ return {1, 0}
61
+ `;
38
62
  export class RedisRateLimitStore {
39
63
  client;
64
+ options;
40
65
  prefix;
41
66
  useEval;
42
67
  constructor(client, options = {}) {
43
68
  this.client = client;
69
+ this.options = options;
44
70
  this.prefix = options.prefix ?? 'nexus-ai-pro:ratelimit:';
45
71
  this.useEval = options.useEval !== false && typeof client.eval === 'function';
46
72
  }
@@ -64,6 +90,14 @@ export class RedisRateLimitStore {
64
90
  }
65
91
  return { count, resetAt: Date.now() + ttl };
66
92
  }
93
+ async gcra(key, emissionMs, burst) {
94
+ if (!this.client.eval || this.options.useEval === false) {
95
+ throw new Error('GCRA rate limiting through Redis needs a client with eval');
96
+ }
97
+ const raw = (await this.client.eval(GCRA_SCRIPT, 1, `${this.prefix}gcra:${key}`, String(emissionMs), String(burst)));
98
+ const allowed = Number(raw?.[0]) === 1;
99
+ return { allowed, retryAt: Date.now() + Math.max(0, Number(raw?.[1] ?? 0)) };
100
+ }
67
101
  async reset(key) {
68
102
  await this.client.del?.(this.prefix + key);
69
103
  }
@@ -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 {
@@ -26,11 +28,12 @@ export declare class NexusRateLimitError extends Error {
26
28
  get retryAfterSeconds(): number | undefined;
27
29
  }
28
30
  /**
29
- * Fixed-window rate limiting, per user, per model, or globally, in memory or through a shared
30
- * store.
31
+ * Rate limiting per user, per model, or globally, in memory or through a shared store, by fixed
32
+ * windows or by GCRA.
31
33
  */
32
34
  export declare class RateLimiter {
33
35
  private buckets;
36
+ private arrivals;
34
37
  /**
35
38
  * Counts one call against a distributed store.
36
39
  *
@@ -43,3 +46,12 @@ export declare class RateLimiter {
43
46
  check(request: RateLimitedRequest, config?: RateLimitConfig): void;
44
47
  private getKey;
45
48
  }
49
+ /**
50
+ * GCRA on one key's theoretical arrival time: the time the key would be idle again at the allowed
51
+ * rate. A call is allowed when that time, after counting it, is no more than `burst` intervals ahead.
52
+ */
53
+ export declare function gcraDecide(tat: number | undefined, now: number, emissionMs: number, burst: number): {
54
+ allowed: boolean;
55
+ retryAt: number;
56
+ tat: number;
57
+ };
@@ -15,6 +15,7 @@ export class NexusRateLimitError extends Error {
15
15
  }
16
16
  export class RateLimiter {
17
17
  buckets = new Map();
18
+ arrivals = new Map();
18
19
  async checkAsync(request, config) {
19
20
  if (!config?.enabled)
20
21
  return;
@@ -24,6 +25,14 @@ export class RateLimiter {
24
25
  return;
25
26
  }
26
27
  const key = this.getKey(request, config);
28
+ if (config.algorithm === 'gcra') {
29
+ if (!store.gcra)
30
+ throw new Error('This rate-limit store cannot decide GCRA; give it gcra(), or use fixed windows');
31
+ const decided = await store.gcra(key, emissionOf(config), burstOf(config));
32
+ if (!decided.allowed)
33
+ throw new NexusRateLimitError(key, decided.retryAt);
34
+ return;
35
+ }
27
36
  const hit = await store.hit(key, config.windowMs);
28
37
  if (hit.count > config.maxRequests) {
29
38
  throw new NexusRateLimitError(key, hit.resetAt);
@@ -34,6 +43,13 @@ export class RateLimiter {
34
43
  return;
35
44
  const key = this.getKey(request, config);
36
45
  const now = Date.now();
46
+ if (config.algorithm === 'gcra') {
47
+ const decided = gcraDecide(this.arrivals.get(key), now, emissionOf(config), burstOf(config));
48
+ if (!decided.allowed)
49
+ throw new NexusRateLimitError(key, decided.retryAt);
50
+ this.arrivals.set(key, decided.tat);
51
+ return;
52
+ }
37
53
  const bucket = this.buckets.get(key);
38
54
  if (!bucket || bucket.resetAt <= now) {
39
55
  this.buckets.set(key, { count: 1, resetAt: now + config.windowMs });
@@ -47,8 +63,23 @@ export class RateLimiter {
47
63
  getKey(request, config) {
48
64
  if (config.key === 'model')
49
65
  return `model:${request.model}`;
66
+ if (config.key === 'tenantId')
67
+ return `tenant:${request.tenantId || 'none'}`;
50
68
  if (config.key === 'global')
51
69
  return 'global';
52
70
  return `user:${request.userId || 'anonymous'}`;
53
71
  }
54
72
  }
73
+ function emissionOf(config) {
74
+ return config.windowMs / Math.max(1, config.maxRequests);
75
+ }
76
+ function burstOf(config) {
77
+ return Math.max(1, config.burst ?? config.maxRequests);
78
+ }
79
+ export function gcraDecide(tat, now, emissionMs, burst) {
80
+ const next = Math.max(tat ?? now, now) + emissionMs;
81
+ const allowAt = next - burst * emissionMs;
82
+ if (allowAt > now)
83
+ return { allowed: false, retryAt: allowAt, tat: tat ?? now };
84
+ return { allowed: true, retryAt: now, tat: next };
85
+ }
@@ -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;