nexus-ai-pro 2.2.0 → 2.4.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 (298) hide show
  1. package/API_STABILITY.md +225 -0
  2. package/CHANGELOG.md +325 -1
  3. package/README.md +20 -6
  4. package/SECURITY.md +140 -0
  5. package/dist/adapter-kit/contracts.d.ts +87 -0
  6. package/dist/adapter-kit/contracts.js +237 -0
  7. package/dist/adapter-kit/errors.d.ts +39 -0
  8. package/dist/adapter-kit/errors.js +52 -0
  9. package/dist/adapter-kit/index.d.ts +144 -0
  10. package/dist/adapter-kit/index.js +178 -0
  11. package/dist/adapter-kit/semver.d.ts +12 -0
  12. package/dist/adapter-kit/semver.js +55 -0
  13. package/dist/agent/capabilities.d.ts +39 -0
  14. package/dist/agent/capabilities.js +36 -0
  15. package/dist/agent/create-agent.d.ts +61 -11
  16. package/dist/agent/create-agent.js +136 -22
  17. package/dist/agent/index.d.ts +3 -2
  18. package/dist/agent/index.js +1 -1
  19. package/dist/agent/middleware/approval.d.ts +26 -0
  20. package/dist/agent/middleware/approval.js +23 -0
  21. package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
  22. package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
  23. package/dist/agent/middleware/context-editor.d.ts +52 -0
  24. package/dist/agent/middleware/context-editor.js +66 -0
  25. package/dist/agent/middleware/filesystem.d.ts +44 -0
  26. package/dist/agent/middleware/filesystem.js +83 -0
  27. package/dist/agent/middleware/index.d.ts +19 -0
  28. package/dist/agent/middleware/index.js +9 -0
  29. package/dist/agent/middleware/limits.d.ts +42 -0
  30. package/dist/agent/middleware/limits.js +52 -0
  31. package/dist/agent/middleware/models.d.ts +48 -0
  32. package/dist/agent/middleware/models.js +44 -0
  33. package/dist/agent/middleware/pii.d.ts +67 -0
  34. package/dist/agent/middleware/pii.js +142 -0
  35. package/dist/agent/middleware/retry.d.ts +66 -0
  36. package/dist/agent/middleware/retry.js +59 -0
  37. package/dist/agent/middleware/shared.d.ts +41 -0
  38. package/dist/agent/middleware/shared.js +73 -0
  39. package/dist/agent/middleware/tool-selector.d.ts +64 -0
  40. package/dist/agent/middleware/tool-selector.js +106 -0
  41. package/dist/agent/permissions.d.ts +113 -0
  42. package/dist/agent/permissions.js +180 -0
  43. package/dist/agent/sandbox.d.ts +151 -0
  44. package/dist/agent/sandbox.js +257 -0
  45. package/dist/agent/tool.d.ts +8 -3
  46. package/dist/agent/tool.js +14 -3
  47. package/dist/core/secure-stream.js +4 -3
  48. package/dist/deep-agent/index.d.ts +109 -0
  49. package/dist/deep-agent/index.js +216 -0
  50. package/dist/embeddings/adapters.js +2 -1
  51. package/dist/evals/judge.js +2 -5
  52. package/dist/evaluate/cache.d.ts +0 -14
  53. package/dist/evaluate/cache.js +0 -30
  54. package/dist/evaluate/file-cache.d.ts +15 -0
  55. package/dist/evaluate/file-cache.js +30 -0
  56. package/dist/evaluate/index.d.ts +2 -1
  57. package/dist/evaluate/index.js +2 -1
  58. package/dist/evaluate/review.js +2 -2
  59. package/dist/evaluate/run.js +2 -2
  60. package/dist/evaluate/version.js +3 -3
  61. package/dist/graph/functional.d.ts +9 -0
  62. package/dist/graph/functional.js +20 -3
  63. package/dist/graph/graph.js +27 -6
  64. package/dist/graph/lint.d.ts +4 -1
  65. package/dist/graph/lint.js +35 -0
  66. package/dist/graph/node-cache.js +3 -3
  67. package/dist/hallucination/rag.d.ts +4 -1
  68. package/dist/hallucination/rag.js +6 -1
  69. package/dist/images/asset-stores.js +2 -3
  70. package/dist/images/asset-support.js +2 -2
  71. package/dist/images/assets.js +1 -2
  72. package/dist/images/evals.js +4 -4
  73. package/dist/insights/proposals.js +1 -2
  74. package/dist/insights/regressions.d.ts +26 -2
  75. package/dist/insights/regressions.js +129 -3
  76. package/dist/loaders/git.js +8 -1
  77. package/dist/loaders/web.js +7 -1
  78. package/dist/mcp/client.d.ts +14 -0
  79. package/dist/mcp/client.js +59 -1
  80. package/dist/mcp/registry.d.ts +6 -0
  81. package/dist/mcp/registry.js +7 -0
  82. package/dist/postgres/fulltext.d.ts +67 -0
  83. package/dist/postgres/fulltext.js +91 -0
  84. package/dist/prompts/file.js +28 -11
  85. package/dist/protocols/a2a.d.ts +235 -0
  86. package/dist/protocols/a2a.js +386 -0
  87. package/dist/protocols/acp.d.ts +87 -0
  88. package/dist/protocols/acp.js +322 -0
  89. package/dist/protocols/ag-ui.d.ts +120 -0
  90. package/dist/protocols/ag-ui.js +180 -0
  91. package/dist/protocols/shared.d.ts +80 -0
  92. package/dist/protocols/shared.js +49 -0
  93. package/dist/providers/anthropic.js +1 -0
  94. package/dist/providers/cohere.js +2 -2
  95. package/dist/providers/google.js +2 -11
  96. package/dist/providers/openai.js +1 -0
  97. package/dist/rag/elasticsearch.d.ts +71 -0
  98. package/dist/rag/elasticsearch.js +130 -0
  99. package/dist/rag/file-ingestion.js +15 -9
  100. package/dist/rag/pipeline.d.ts +140 -0
  101. package/dist/rag/pipeline.js +187 -0
  102. package/dist/rag/rerankers.d.ts +92 -0
  103. package/dist/rag/rerankers.js +97 -0
  104. package/dist/rag/retrievers.d.ts +12 -1
  105. package/dist/runtime/detect.d.ts +17 -0
  106. package/dist/runtime/detect.js +16 -0
  107. package/dist/runtime/index.d.ts +19 -0
  108. package/dist/runtime/index.js +7 -0
  109. package/dist/security/injection-detector.d.ts +2 -0
  110. package/dist/security/injection-detector.js +7 -1
  111. package/dist/security/input-guard.js +5 -2
  112. package/dist/security/output-guard.js +8 -4
  113. package/dist/security/pii-detector.js +22 -6
  114. package/dist/security/prompt-hardening.js +6 -2
  115. package/dist/security/semantic-injection-classifier.d.ts +2 -1
  116. package/dist/security/semantic-injection-classifier.js +31 -35
  117. package/dist/security/upload-scanner.d.ts +8 -0
  118. package/dist/security/upload-scanner.js +51 -9
  119. package/dist/server/assistant.d.ts +5 -1
  120. package/dist/server/assistant.js +4 -2
  121. package/dist/server/auth.d.ts +250 -0
  122. package/dist/server/auth.js +392 -0
  123. package/dist/server/deployments.d.ts +63 -1
  124. package/dist/server/deployments.js +75 -2
  125. package/dist/server/runs.d.ts +9 -0
  126. package/dist/server/runs.js +20 -0
  127. package/dist/server/server.js +5 -0
  128. package/dist/store/memory.d.ts +10 -0
  129. package/dist/store/memory.js +10 -0
  130. package/dist/testing/record.js +2 -2
  131. package/dist/tracing/stores.js +2 -1
  132. package/dist/tracing/tracer.js +2 -2
  133. package/dist/types/config.d.ts +20 -0
  134. package/dist/types/graph.d.ts +38 -0
  135. package/dist/types/messages.d.ts +36 -2
  136. package/dist/types/principal.d.ts +21 -0
  137. package/dist/types/principal.js +1 -0
  138. package/dist/types/server.d.ts +8 -9
  139. package/dist/utils/base64.d.ts +4 -0
  140. package/dist/utils/base64.js +21 -0
  141. package/dist/utils/checksums.d.ts +22 -0
  142. package/dist/utils/checksums.js +39 -0
  143. package/dist/utils/ids.d.ts +8 -0
  144. package/dist/utils/ids.js +26 -2
  145. package/dist/utils/sha256.d.ts +22 -0
  146. package/dist/utils/sha256.js +120 -0
  147. package/dist/utils/tenant.d.ts +11 -0
  148. package/dist/utils/tenant.js +14 -0
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/dist-cjs/adapter-kit/contracts.d.ts +87 -0
  152. package/dist-cjs/adapter-kit/contracts.js +243 -0
  153. package/dist-cjs/adapter-kit/errors.d.ts +39 -0
  154. package/dist-cjs/adapter-kit/errors.js +57 -0
  155. package/dist-cjs/adapter-kit/index.d.ts +144 -0
  156. package/dist-cjs/adapter-kit/index.js +194 -0
  157. package/dist-cjs/adapter-kit/semver.d.ts +12 -0
  158. package/dist-cjs/adapter-kit/semver.js +59 -0
  159. package/dist-cjs/agent/capabilities.d.ts +39 -0
  160. package/dist-cjs/agent/capabilities.js +41 -0
  161. package/dist-cjs/agent/create-agent.d.ts +61 -11
  162. package/dist-cjs/agent/create-agent.js +136 -22
  163. package/dist-cjs/agent/index.d.ts +3 -2
  164. package/dist-cjs/agent/index.js +4 -4
  165. package/dist-cjs/agent/middleware/approval.d.ts +26 -0
  166. package/dist-cjs/agent/middleware/approval.js +26 -0
  167. package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
  168. package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
  169. package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
  170. package/dist-cjs/agent/middleware/context-editor.js +70 -0
  171. package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
  172. package/dist-cjs/agent/middleware/filesystem.js +86 -0
  173. package/dist-cjs/agent/middleware/index.d.ts +19 -0
  174. package/dist-cjs/agent/middleware/index.js +28 -0
  175. package/dist-cjs/agent/middleware/limits.d.ts +42 -0
  176. package/dist-cjs/agent/middleware/limits.js +57 -0
  177. package/dist-cjs/agent/middleware/models.d.ts +48 -0
  178. package/dist-cjs/agent/middleware/models.js +48 -0
  179. package/dist-cjs/agent/middleware/pii.d.ts +67 -0
  180. package/dist-cjs/agent/middleware/pii.js +147 -0
  181. package/dist-cjs/agent/middleware/retry.d.ts +66 -0
  182. package/dist-cjs/agent/middleware/retry.js +63 -0
  183. package/dist-cjs/agent/middleware/shared.d.ts +41 -0
  184. package/dist-cjs/agent/middleware/shared.js +83 -0
  185. package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
  186. package/dist-cjs/agent/middleware/tool-selector.js +109 -0
  187. package/dist-cjs/agent/permissions.d.ts +113 -0
  188. package/dist-cjs/agent/permissions.js +189 -0
  189. package/dist-cjs/agent/sandbox.d.ts +151 -0
  190. package/dist-cjs/agent/sandbox.js +297 -0
  191. package/dist-cjs/agent/tool.d.ts +8 -3
  192. package/dist-cjs/agent/tool.js +14 -3
  193. package/dist-cjs/core/secure-stream.js +4 -3
  194. package/dist-cjs/deep-agent/index.d.ts +109 -0
  195. package/dist-cjs/deep-agent/index.js +219 -0
  196. package/dist-cjs/embeddings/adapters.js +2 -1
  197. package/dist-cjs/evals/judge.js +2 -5
  198. package/dist-cjs/evaluate/cache.d.ts +0 -14
  199. package/dist-cjs/evaluate/cache.js +1 -65
  200. package/dist-cjs/evaluate/file-cache.d.ts +15 -0
  201. package/dist-cjs/evaluate/file-cache.js +67 -0
  202. package/dist-cjs/evaluate/index.d.ts +2 -1
  203. package/dist-cjs/evaluate/index.js +3 -2
  204. package/dist-cjs/evaluate/review.js +2 -2
  205. package/dist-cjs/evaluate/run.js +2 -2
  206. package/dist-cjs/evaluate/version.js +3 -3
  207. package/dist-cjs/graph/functional.d.ts +9 -0
  208. package/dist-cjs/graph/functional.js +20 -3
  209. package/dist-cjs/graph/graph.js +27 -6
  210. package/dist-cjs/graph/lint.d.ts +4 -1
  211. package/dist-cjs/graph/lint.js +35 -0
  212. package/dist-cjs/graph/node-cache.js +3 -3
  213. package/dist-cjs/hallucination/rag.d.ts +4 -1
  214. package/dist-cjs/hallucination/rag.js +6 -1
  215. package/dist-cjs/images/asset-stores.js +2 -3
  216. package/dist-cjs/images/asset-support.js +2 -2
  217. package/dist-cjs/images/assets.js +1 -2
  218. package/dist-cjs/images/evals.js +4 -4
  219. package/dist-cjs/insights/proposals.js +1 -2
  220. package/dist-cjs/insights/regressions.d.ts +26 -2
  221. package/dist-cjs/insights/regressions.js +129 -3
  222. package/dist-cjs/loaders/git.js +8 -1
  223. package/dist-cjs/loaders/web.js +7 -1
  224. package/dist-cjs/mcp/client.d.ts +14 -0
  225. package/dist-cjs/mcp/client.js +60 -1
  226. package/dist-cjs/mcp/registry.d.ts +6 -0
  227. package/dist-cjs/mcp/registry.js +7 -0
  228. package/dist-cjs/postgres/fulltext.d.ts +67 -0
  229. package/dist-cjs/postgres/fulltext.js +129 -0
  230. package/dist-cjs/prompts/file.js +28 -11
  231. package/dist-cjs/protocols/a2a.d.ts +235 -0
  232. package/dist-cjs/protocols/a2a.js +393 -0
  233. package/dist-cjs/protocols/acp.d.ts +87 -0
  234. package/dist-cjs/protocols/acp.js +326 -0
  235. package/dist-cjs/protocols/ag-ui.d.ts +120 -0
  236. package/dist-cjs/protocols/ag-ui.js +186 -0
  237. package/dist-cjs/protocols/shared.d.ts +80 -0
  238. package/dist-cjs/protocols/shared.js +59 -0
  239. package/dist-cjs/providers/anthropic.js +1 -0
  240. package/dist-cjs/providers/cohere.js +2 -2
  241. package/dist-cjs/providers/google.js +3 -12
  242. package/dist-cjs/providers/openai.js +1 -0
  243. package/dist-cjs/rag/elasticsearch.d.ts +71 -0
  244. package/dist-cjs/rag/elasticsearch.js +135 -0
  245. package/dist-cjs/rag/file-ingestion.js +14 -8
  246. package/dist-cjs/rag/pipeline.d.ts +140 -0
  247. package/dist-cjs/rag/pipeline.js +191 -0
  248. package/dist-cjs/rag/rerankers.d.ts +92 -0
  249. package/dist-cjs/rag/rerankers.js +107 -0
  250. package/dist-cjs/rag/retrievers.d.ts +12 -1
  251. package/dist-cjs/runtime/detect.d.ts +17 -0
  252. package/dist-cjs/runtime/detect.js +19 -0
  253. package/dist-cjs/runtime/index.d.ts +19 -0
  254. package/dist-cjs/runtime/index.js +31 -0
  255. package/dist-cjs/security/injection-detector.d.ts +2 -0
  256. package/dist-cjs/security/injection-detector.js +7 -1
  257. package/dist-cjs/security/input-guard.js +5 -2
  258. package/dist-cjs/security/output-guard.js +8 -4
  259. package/dist-cjs/security/pii-detector.js +22 -6
  260. package/dist-cjs/security/prompt-hardening.js +6 -2
  261. package/dist-cjs/security/semantic-injection-classifier.d.ts +2 -1
  262. package/dist-cjs/security/semantic-injection-classifier.js +31 -35
  263. package/dist-cjs/security/upload-scanner.d.ts +8 -0
  264. package/dist-cjs/security/upload-scanner.js +53 -9
  265. package/dist-cjs/server/assistant.d.ts +5 -1
  266. package/dist-cjs/server/assistant.js +4 -2
  267. package/dist-cjs/server/auth.d.ts +250 -0
  268. package/dist-cjs/server/auth.js +402 -0
  269. package/dist-cjs/server/deployments.d.ts +63 -1
  270. package/dist-cjs/server/deployments.js +75 -2
  271. package/dist-cjs/server/runs.d.ts +9 -0
  272. package/dist-cjs/server/runs.js +20 -0
  273. package/dist-cjs/server/server.js +5 -0
  274. package/dist-cjs/store/memory.d.ts +10 -0
  275. package/dist-cjs/store/memory.js +10 -0
  276. package/dist-cjs/testing/record.js +2 -2
  277. package/dist-cjs/tracing/stores.js +2 -1
  278. package/dist-cjs/tracing/tracer.js +2 -2
  279. package/dist-cjs/types/config.d.ts +20 -0
  280. package/dist-cjs/types/graph.d.ts +38 -0
  281. package/dist-cjs/types/messages.d.ts +36 -2
  282. package/dist-cjs/types/principal.d.ts +21 -0
  283. package/dist-cjs/types/principal.js +2 -0
  284. package/dist-cjs/types/server.d.ts +8 -9
  285. package/dist-cjs/utils/base64.d.ts +4 -0
  286. package/dist-cjs/utils/base64.js +25 -0
  287. package/dist-cjs/utils/checksums.d.ts +22 -0
  288. package/dist-cjs/utils/checksums.js +46 -0
  289. package/dist-cjs/utils/ids.d.ts +8 -0
  290. package/dist-cjs/utils/ids.js +28 -2
  291. package/dist-cjs/utils/sha256.d.ts +22 -0
  292. package/dist-cjs/utils/sha256.js +125 -0
  293. package/dist-cjs/utils/tenant.d.ts +11 -0
  294. package/dist-cjs/utils/tenant.js +15 -0
  295. package/dist-cjs/version.d.ts +1 -1
  296. package/dist-cjs/version.js +1 -1
  297. package/llms.txt +7 -3
  298. package/package.json +193 -6
@@ -33,7 +33,10 @@ export interface CompareRunsOptions {
33
33
  metrics?: readonly RegressionMetric[];
34
34
  /** Feedback keys whose mean score is compared, such as `helpfulness`. */
35
35
  feedback?: readonly string[];
36
- /** Runs, or feedback scores, each side needs before it is judged. Defaults to 20. */
36
+ /**
37
+ * Runs each side needs before it is judged; for a feedback score, runs that have one. Defaults to
38
+ * 20. A run counts once per feedback key, however many times it was rated.
39
+ */
37
40
  minRuns?: number;
38
41
  /** A rise in p95 latency, relative, that counts. Defaults to 0.25, a quarter slower. */
39
42
  latencyIncrease?: number;
@@ -44,6 +47,22 @@ export interface CompareRunsOptions {
44
47
  minLatencyChangeMs?: number;
45
48
  /** A rise in mean cost, relative, that counts. Defaults to 0.25. */
46
49
  costIncrease?: number;
50
+ /**
51
+ * Judges every metric the way an evaluation comparison does, at this confidence, such as 0.95. Each
52
+ * metric is resampled by bootstrap, and it regresses only when two things hold: the change's
53
+ * interval does not reach zero, and the change is at least that metric's minimum effect. Noise and
54
+ * trivia both stay below the bar. Without it, the error rate uses a z-test at 95%, and latency and
55
+ * cost use their margins alone.
56
+ */
57
+ confidence?: number;
58
+ /** The smallest rise in the error rate, absolute, that counts under `confidence`. Defaults to 0.02. */
59
+ minErrorRateIncrease?: number;
60
+ /** The smallest fall in a feedback score's mean, absolute, that counts under `confidence`. Defaults to 0.05. */
61
+ minFeedbackDrop?: number;
62
+ /** Bootstrap resamples under `confidence`. Defaults to 1,000. */
63
+ resamples?: number;
64
+ /** Seeds the resampling, so the same runs always get the same verdict. Defaults to 1. */
65
+ seed?: number;
47
66
  }
48
67
  /** Options for `detectRegressions()`. */
49
68
  export interface DetectRegressionsOptions {
@@ -61,7 +80,10 @@ export interface DetectRegressionsOptions {
61
80
  metrics?: readonly RegressionMetric[];
62
81
  /** Feedback keys whose mean score is compared, such as `helpfulness`. */
63
82
  feedback?: readonly string[];
64
- /** Runs, or feedback scores, each window needs before a group is judged. Defaults to 20. */
83
+ /**
84
+ * Runs each window needs before a group is judged; for a feedback score, runs that have one.
85
+ * Defaults to 20.
86
+ */
65
87
  minRuns?: number;
66
88
  /** A rise in p95 latency, relative, that counts. Defaults to 0.25, a quarter slower. */
67
89
  latencyIncrease?: number;
@@ -96,6 +118,8 @@ export interface Regression {
96
118
  };
97
119
  /** One readable line. */
98
120
  summary: string;
121
+ /** The change's interval, in the units of `change`, when the comparison was judged with `confidence`. */
122
+ interval?: [number, number];
99
123
  }
100
124
  /**
101
125
  * Compares a current window of traces with a baseline window and reports what got worse, per run
@@ -1,3 +1,12 @@
1
+ function runScores(runs, key) {
2
+ return runs.flatMap((run) => {
3
+ const given = (run.feedback ?? [])
4
+ .filter((item) => item.key === key)
5
+ .map((item) => item.score)
6
+ .filter(isNumber);
7
+ return given.length ? [given.reduce((sum, score) => sum + score, 0) / given.length] : [];
8
+ });
9
+ }
1
10
  export async function detectRegressions(options) {
2
11
  const read = async (window) => {
3
12
  const runs = await options.store.query({
@@ -27,6 +36,8 @@ export async function detectRegressions(options) {
27
36
  return regressions;
28
37
  }
29
38
  export function compareRuns(baseline, current, options = {}) {
39
+ if (options.confidence !== undefined)
40
+ return judgedRegressions(baseline, current, options, options.confidence);
30
41
  const name = options.group ?? 'runs';
31
42
  const minRuns = options.minRuns ?? 20;
32
43
  const metrics = new Set(options.metrics ?? ['error-rate', 'latency', 'cost']);
@@ -90,9 +101,7 @@ export function compareRuns(baseline, current, options = {}) {
90
101
  }
91
102
  }
92
103
  for (const feedbackKey of options.feedback ?? []) {
93
- const scores = (runs) => runs
94
- .flatMap((run) => (run.feedback ?? []).filter((item) => item.key === feedbackKey).map((item) => item.score))
95
- .filter(isNumber);
104
+ const scores = (runs) => runScores(runs, feedbackKey);
96
105
  const before = scores(baseline);
97
106
  const after = scores(current);
98
107
  if (before.length < minRuns || after.length < minRuns)
@@ -114,6 +123,123 @@ export function compareRuns(baseline, current, options = {}) {
114
123
  }
115
124
  return regressions;
116
125
  }
126
+ function judgedRegressions(baseline, current, options, confidence) {
127
+ if (!(confidence > 0 && confidence < 1))
128
+ throw new RangeError('confidence is between 0 and 1, such as 0.95');
129
+ const name = options.group ?? 'runs';
130
+ const minRuns = options.minRuns ?? 20;
131
+ const metrics = new Set(options.metrics ?? ['error-rate', 'latency', 'cost']);
132
+ const judge = (before, after, shape) => before.length >= minRuns && after.length >= minRuns
133
+ ? metricVerdict(before, after, {
134
+ ...shape,
135
+ confidence,
136
+ resamples: options.resamples ?? 1_000,
137
+ seed: options.seed ?? 1,
138
+ })
139
+ : undefined;
140
+ const regressions = [];
141
+ const add = (metric, verdict, counts, line) => {
142
+ if (!verdict?.regressed)
143
+ return;
144
+ regressions.push({
145
+ group: name,
146
+ metric,
147
+ baseline: verdict.baseline,
148
+ current: verdict.current,
149
+ change: verdict.change,
150
+ samples: counts,
151
+ interval: verdict.interval,
152
+ summary: `${name}: ${line} (${Math.round(confidence * 100)}% interval ${formatInterval(verdict.interval, metric)})`,
153
+ });
154
+ };
155
+ const counts = { baseline: baseline.length, current: current.length };
156
+ if (metrics.has('error-rate')) {
157
+ const errors = (runs) => runs.map((run) => (run.status === 'error' ? 1 : 0));
158
+ const verdict = judge(errors(baseline), errors(current), {
159
+ statistic: meanOf,
160
+ relative: false,
161
+ worse: 'rise',
162
+ minEffect: options.minErrorRateIncrease ?? 0.02,
163
+ });
164
+ add('error-rate', verdict, counts, `error rate rose from ${percent(verdict?.baseline ?? 0)} to ${percent(verdict?.current ?? 0)}`);
165
+ }
166
+ if (metrics.has('latency')) {
167
+ const latencies = (runs) => runs.map((run) => run.latencyMs).filter(isNumber);
168
+ const before = latencies(baseline);
169
+ const after = latencies(current);
170
+ const verdict = judge(before, after, {
171
+ statistic: (values) => percentile(values, 0.95) ?? 0,
172
+ relative: true,
173
+ worse: 'rise',
174
+ minEffect: options.latencyIncrease ?? 0.25,
175
+ });
176
+ const large = verdict && verdict.current - verdict.baseline >= (options.minLatencyChangeMs ?? 0);
177
+ add('latency-p95', large ? verdict : undefined, { baseline: before.length, current: after.length }, `p95 latency rose from ${Math.round(verdict?.baseline ?? 0)}ms to ${Math.round(verdict?.current ?? 0)}ms`);
178
+ }
179
+ if (metrics.has('cost')) {
180
+ const costs = (runs) => runs.map((run) => run.cost).filter(isNumber);
181
+ const before = costs(baseline);
182
+ const after = costs(current);
183
+ const verdict = judge(before, after, {
184
+ statistic: meanOf,
185
+ relative: true,
186
+ worse: 'rise',
187
+ minEffect: options.costIncrease ?? 0.25,
188
+ });
189
+ add('cost', verdict, { baseline: before.length, current: after.length }, `mean cost rose from $${(verdict?.baseline ?? 0).toFixed(4)} to $${(verdict?.current ?? 0).toFixed(4)}`);
190
+ }
191
+ for (const feedbackKey of options.feedback ?? []) {
192
+ const scores = (runs) => runScores(runs, feedbackKey);
193
+ const before = scores(baseline);
194
+ const after = scores(current);
195
+ const verdict = judge(before, after, {
196
+ statistic: meanOf,
197
+ relative: false,
198
+ worse: 'fall',
199
+ minEffect: options.minFeedbackDrop ?? 0.05,
200
+ });
201
+ add(`feedback:${feedbackKey}`, verdict, { baseline: before.length, current: after.length }, `${feedbackKey} fell from ${(verdict?.baseline ?? 0).toFixed(2)} to ${(verdict?.current ?? 0).toFixed(2)}`);
202
+ }
203
+ return regressions;
204
+ }
205
+ function metricVerdict(baseline, current, options) {
206
+ const change = (before, after) => options.relative ? (before === 0 ? 0 : after / before - 1) : after - before;
207
+ const before = options.statistic(baseline);
208
+ const after = options.statistic(current);
209
+ const random = seeded(options.seed);
210
+ const resample = (values) => Array.from({ length: values.length }, () => values[Math.floor(random() * values.length)]);
211
+ const draws = [];
212
+ for (let index = 0; index < options.resamples; index += 1) {
213
+ draws.push(change(options.statistic(resample(baseline)), options.statistic(resample(current))));
214
+ }
215
+ draws.sort((a, b) => a - b);
216
+ const tail = (1 - options.confidence) / 2;
217
+ const at = (quantile) => draws[Math.min(draws.length - 1, Math.max(0, Math.floor(quantile * draws.length)))];
218
+ const interval = [at(tail), at(1 - tail)];
219
+ const observed = change(before, after);
220
+ const regressed = options.worse === 'rise'
221
+ ? observed >= options.minEffect && interval[0] > 0
222
+ : -observed >= options.minEffect && interval[1] < 0;
223
+ return { baseline: before, current: after, change: observed, interval, regressed };
224
+ }
225
+ function seeded(seed) {
226
+ let state = seed >>> 0;
227
+ return () => {
228
+ state = (state + 0x6d2b79f5) >>> 0;
229
+ let value = state;
230
+ value = Math.imul(value ^ (value >>> 15), value | 1);
231
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
232
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
233
+ };
234
+ }
235
+ function meanOf(values) {
236
+ return mean(values) ?? 0;
237
+ }
238
+ function formatInterval(interval, metric) {
239
+ const relative = metric === 'latency-p95' || metric === 'cost';
240
+ const show = (value) => (relative ? `${value >= 0 ? '+' : ''}${Math.round(value * 100)}%` : value.toFixed(3));
241
+ return `${show(interval[0])} to ${show(interval[1])}`;
242
+ }
117
243
  function isNumber(value) {
118
244
  return typeof value === 'number' && Number.isFinite(value);
119
245
  }
@@ -32,6 +32,8 @@ export async function* loadGitRepository(repository, options = {}) {
32
32
  const files = (await git(['ls-files', '-z'], directory, options)).split('\0').filter(Boolean).sort();
33
33
  const extensions = options.extensions && new Set(options.extensions.map((extension) => extension.toLowerCase()));
34
34
  const maxFileBytes = options.maxFileBytes ?? 1_000_000;
35
+ const { lstat, realpath } = await import('node:fs/promises');
36
+ const checkout = await realpath(directory);
35
37
  for (const file of files) {
36
38
  const extension = path.extname(file).toLowerCase();
37
39
  if (extensions && !extensions.has(extension))
@@ -40,7 +42,12 @@ export async function* loadGitRepository(repository, options = {}) {
40
42
  continue;
41
43
  let bytes;
42
44
  try {
43
- bytes = new Uint8Array(await readFile(path.join(directory, file)));
45
+ const full = path.join(directory, file);
46
+ if ((await lstat(full)).isSymbolicLink())
47
+ continue;
48
+ if (!(await realpath(full)).startsWith(checkout + path.sep))
49
+ continue;
50
+ bytes = new Uint8Array(await readFile(full));
44
51
  }
45
52
  catch {
46
53
  continue;
@@ -1,5 +1,6 @@
1
1
  import { parseUrl, safeFetch } from '../utils/safe-fetch.js';
2
2
  import { htmlToText } from './html.js';
3
+ const MAX_SITEMAP_BYTES = 50 * 1024 * 1024;
3
4
  export class WebLoaderError extends Error {
4
5
  url;
5
6
  status;
@@ -49,7 +50,12 @@ export async function sitemapUrls(url, options = {}) {
49
50
  let xml;
50
51
  if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
51
52
  const { gunzipSync } = await import('node:zlib');
52
- xml = gunzipSync(bytes).toString('utf8');
53
+ try {
54
+ xml = gunzipSync(bytes, { maxOutputLength: MAX_SITEMAP_BYTES }).toString('utf8');
55
+ }
56
+ catch (error) {
57
+ throw new WebLoaderError(`${sitemap} does not decompress to a sitemap of at most 50 MB: ${error instanceof Error ? error.message : String(error)}`, sitemap);
58
+ }
53
59
  }
54
60
  else
55
61
  xml = new TextDecoder().decode(bytes);
@@ -122,7 +122,21 @@ export interface StdioClientOptions {
122
122
  env?: Record<string, string>;
123
123
  /** Working directory for the process. */
124
124
  cwd?: string;
125
+ /**
126
+ * Which of this process's environment variables the server inherits, besides `env`. By default,
127
+ * only what a process needs to start: the path, the home and temporary directories, the user, the
128
+ * locale and terminal, the Windows system variables, proxies, and certificate settings. A server is
129
+ * often third-party code, and must not see this process's API keys or database URLs. A list adds
130
+ * the variables it names, `true` passes the whole environment, as releases before 2.4 did, and
131
+ * `false` passes nothing but `env`.
132
+ */
133
+ inheritEnv?: boolean | readonly string[];
125
134
  }
135
+ /**
136
+ * The environment a server process starts with. Names compare without case, as Windows compares
137
+ * them, and `LC_` locale settings are kept with the rest of the locale.
138
+ */
139
+ export declare function serverEnvironment(source: Record<string, string | undefined>, options?: Pick<StdioClientOptions, 'env' | 'inheritEnv'>): Record<string, string>;
126
140
  /**
127
141
  * Runs an MCP server as a child process and speaks to it over its stdin and stdout.
128
142
  *
@@ -131,6 +131,64 @@ export class McpClient {
131
131
  this.settle(id, undefined, error);
132
132
  }
133
133
  }
134
+ const INHERITED_ENV = new Set([
135
+ 'PATH',
136
+ 'PATHEXT',
137
+ 'HOME',
138
+ 'USER',
139
+ 'USERNAME',
140
+ 'LOGNAME',
141
+ 'SHELL',
142
+ 'TERM',
143
+ 'LANG',
144
+ 'LANGUAGE',
145
+ 'TZ',
146
+ 'TMPDIR',
147
+ 'TEMP',
148
+ 'TMP',
149
+ 'XDG_CONFIG_HOME',
150
+ 'XDG_CACHE_HOME',
151
+ 'XDG_DATA_HOME',
152
+ 'XDG_RUNTIME_DIR',
153
+ 'SYSTEMROOT',
154
+ 'SYSTEMDRIVE',
155
+ 'WINDIR',
156
+ 'COMSPEC',
157
+ 'APPDATA',
158
+ 'LOCALAPPDATA',
159
+ 'USERPROFILE',
160
+ 'HOMEDRIVE',
161
+ 'HOMEPATH',
162
+ 'PROGRAMDATA',
163
+ 'PROGRAMFILES',
164
+ 'PROGRAMFILES(X86)',
165
+ 'COMMONPROGRAMFILES',
166
+ 'PROCESSOR_ARCHITECTURE',
167
+ 'NUMBER_OF_PROCESSORS',
168
+ 'OS',
169
+ 'HTTP_PROXY',
170
+ 'HTTPS_PROXY',
171
+ 'NO_PROXY',
172
+ 'ALL_PROXY',
173
+ 'NODE_EXTRA_CA_CERTS',
174
+ 'SSL_CERT_FILE',
175
+ 'SSL_CERT_DIR',
176
+ ]);
177
+ export function serverEnvironment(source, options = {}) {
178
+ if (options.inheritEnv === false)
179
+ return { ...options.env };
180
+ const named = new Set((Array.isArray(options.inheritEnv) ? options.inheritEnv : []).map((name) => name.toUpperCase()));
181
+ const inherited = {};
182
+ for (const [name, value] of Object.entries(source)) {
183
+ if (value === undefined)
184
+ continue;
185
+ const upper = name.toUpperCase();
186
+ if (options.inheritEnv === true || INHERITED_ENV.has(upper) || upper.startsWith('LC_') || named.has(upper)) {
187
+ inherited[name] = value;
188
+ }
189
+ }
190
+ return { ...inherited, ...options.env };
191
+ }
134
192
  export function createStdioTransport(options) {
135
193
  let child;
136
194
  const decoder = new LineDecoder();
@@ -141,7 +199,7 @@ export function createStdioTransport(options) {
141
199
  const { spawn } = await import('node:child_process');
142
200
  const spawned = spawn(options.command, options.args ?? [], {
143
201
  cwd: options.cwd,
144
- env: { ...process.env, ...options.env },
202
+ env: serverEnvironment(process.env, options),
145
203
  stdio: ['pipe', 'pipe', 'inherit'],
146
204
  });
147
205
  spawned.stdout.setEncoding('utf8');
@@ -15,6 +15,12 @@ export interface McpServerConfig {
15
15
  env?: Record<string, string>;
16
16
  /** Working directory for the process. */
17
17
  cwd?: string;
18
+ /**
19
+ * Which of this process's own environment variables a local server inherits, besides `env`. By
20
+ * default, only what a process needs to start, never an API key; a list adds variables by name,
21
+ * `true` passes the whole environment, and `false` nothing but `env`.
22
+ */
23
+ inheritEnv?: boolean | string[];
18
24
  /** The endpoint of a remote server, reached over HTTP. */
19
25
  url?: string;
20
26
  /** Headers sent to a remote server, such as authorization. */
@@ -59,6 +59,7 @@ export class McpRegistry {
59
59
  ...(resolved.args ? { args: resolved.args } : {}),
60
60
  ...(resolved.env ? { env: resolved.env } : {}),
61
61
  ...(resolved.cwd ? { cwd: resolved.cwd } : {}),
62
+ ...(resolved.inheritEnv !== undefined ? { inheritEnv: resolved.inheritEnv } : {}),
62
63
  })
63
64
  : createHttpTransport({
64
65
  url: resolved.url,
@@ -163,6 +164,12 @@ export function validateMcpConfig(value) {
163
164
  throw new McpRegistryError(`Server "${name}": "${field}" is a list of strings`, name);
164
165
  }
165
166
  }
167
+ const inherit = server.inheritEnv;
168
+ if (inherit !== undefined &&
169
+ typeof inherit !== 'boolean' &&
170
+ !(Array.isArray(inherit) && inherit.every((item) => typeof item === 'string'))) {
171
+ throw new McpRegistryError(`Server "${name}": "inheritEnv" is true, false, or a list of names`, name);
172
+ }
166
173
  for (const field of ['env', 'headers']) {
167
174
  const map = server[field];
168
175
  if (map !== undefined &&
@@ -0,0 +1,67 @@
1
+ import type { RagChunk } from '../hallucination/rag.js';
2
+ import type { VectorSearchResult } from '../hallucination/retrieval.js';
3
+ import type { RetrieveOptions, SparseRetriever } from '../rag/retrievers.js';
4
+ import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
5
+ import { type PostgresLikeClient } from './client.js';
6
+ import type { PostgresMigrateOptions } from './migrations.js';
7
+ /** Options for `PostgresKeywordIndex`. */
8
+ export interface PostgresKeywordIndexOptions {
9
+ /** Table name, optionally schema-qualified. Defaults to `nexus_keywords`. */
10
+ table?: string;
11
+ /**
12
+ * Searches the table of a `PostgresVectorStore` instead of keeping chunks of its own. `migrate()`
13
+ * adds a generated search column and its index to that table, the store's writes keep it current,
14
+ * and `add()` and `delete()` leave writing to the store. One copy of every chunk, and keyword and
15
+ * vector search can never disagree about what exists.
16
+ */
17
+ shared?: boolean;
18
+ /** The text search configuration: `english`, `simple`, `german`, and so on. Defaults to `english`. */
19
+ language?: string;
20
+ /**
21
+ * `any` (the default) finds chunks with any of the query's terms, ranked by how many and how close,
22
+ * as keyword search for retrieval should. `all` needs every term, read as a web search query, with
23
+ * quoted phrases and `-` for exclusion.
24
+ */
25
+ match?: 'any' | 'all';
26
+ }
27
+ /**
28
+ * The versioned schema, which `migrate()` applies. Version 1 creates the table with a generated
29
+ * `tsvector` column and a GIN index, or, `shared`, adds the column and the index to a vector table.
30
+ */
31
+ export declare function keywordIndexMigrations(options?: PostgresKeywordIndexOptions): SchemaMigration[];
32
+ /**
33
+ * Keyword search in Postgres, with its full-text search: stemmed terms, a GIN index, and
34
+ * `ts_rank_cd` ranking, so only the best matches leave the database however large the table.
35
+ *
36
+ * A `SparseRetriever`, so `hybridRetriever()` fuses it with `PostgresVectorStore`. With `shared`, it
37
+ * searches the vector store's own table, and the hybrid search runs over one copy of the data. A
38
+ * metadata filter runs in SQL, with the same meaning as the vector store's.
39
+ *
40
+ * ```ts
41
+ * const vectors = new PostgresVectorStore(client, { dimensions: 1536, embed });
42
+ * const keywords = new PostgresKeywordIndex(client, { table: 'nexus_vectors', shared: true });
43
+ * await vectors.migrate();
44
+ * await keywords.migrate();
45
+ * const retriever = hybridRetriever([vectorRetriever(vectors), keywords]);
46
+ * ```
47
+ */
48
+ export declare class PostgresKeywordIndex implements SparseRetriever {
49
+ private readonly client;
50
+ private readonly options;
51
+ private readonly table;
52
+ private readonly language;
53
+ constructor(client: PostgresLikeClient, options?: PostgresKeywordIndexOptions);
54
+ /**
55
+ * Applies this index's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
56
+ * Never runs implicitly. With `shared`, run the vector store's `migrate()` first.
57
+ */
58
+ migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
59
+ /** Adds chunks, or replaces those whose id exists. With `shared`, does nothing: the vector store writes. */
60
+ add(chunks: readonly RagChunk[]): Promise<void>;
61
+ /** Removes chunks by id. With `shared`, does nothing: the vector store deletes. */
62
+ delete(ids: readonly string[]): Promise<void>;
63
+ /** The chunks that best match a query's terms, best first, scored from 0 to 1. */
64
+ retrieve(query: string, options?: RetrieveOptions): Promise<VectorSearchResult[]>;
65
+ /** Chunks held. */
66
+ size(): Promise<number>;
67
+ }
@@ -0,0 +1,91 @@
1
+ import { fromJson, quoteDerived, quoteTable } from './client.js';
2
+ import { SqlParams, textArray } from './sql.js';
3
+ const DEFAULT_TABLE = 'nexus_keywords';
4
+ function languageOf(options) {
5
+ const language = options.language ?? 'english';
6
+ if (!/^[a-z_]{1,63}$/.test(language))
7
+ throw new RangeError(`"${language}" is not a text search configuration name`);
8
+ return language;
9
+ }
10
+ export function keywordIndexMigrations(options = {}) {
11
+ const name = options.table ?? DEFAULT_TABLE;
12
+ const table = quoteTable(name);
13
+ const generated = `fts tsvector GENERATED ALWAYS AS (to_tsvector('${languageOf(options)}'::regconfig, content)) STORED`;
14
+ return [
15
+ {
16
+ component: `keywords:${name}`,
17
+ version: 1,
18
+ name: options.shared ? 'add full-text search to a vector table' : 'create the keyword table',
19
+ statements: [
20
+ options.shared
21
+ ? `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${generated}`
22
+ : `CREATE TABLE IF NOT EXISTS ${table} (
23
+ id text PRIMARY KEY,
24
+ content text NOT NULL,
25
+ source text,
26
+ metadata jsonb,
27
+ ${generated}
28
+ )`,
29
+ `CREATE INDEX IF NOT EXISTS ${quoteDerived(name, 'fts')} ON ${table} USING gin (fts)`,
30
+ ],
31
+ },
32
+ ];
33
+ }
34
+ export class PostgresKeywordIndex {
35
+ client;
36
+ options;
37
+ table;
38
+ language;
39
+ constructor(client, options = {}) {
40
+ this.client = client;
41
+ this.options = options;
42
+ this.table = quoteTable(options.table ?? DEFAULT_TABLE);
43
+ this.language = languageOf(options);
44
+ }
45
+ async migrate(options = {}) {
46
+ const { applyPostgresMigrations } = await import('./migrations.js');
47
+ return applyPostgresMigrations(this.client, keywordIndexMigrations(this.options), options);
48
+ }
49
+ async add(chunks) {
50
+ if (this.options.shared || chunks.length === 0)
51
+ return;
52
+ const params = new SqlParams();
53
+ const rows = chunks.map((chunk) => `(${params.add(chunk.id)}, ${params.add(chunk.content)}, ${params.add(chunk.source ?? null)}, ${params.add(chunk.metadata === undefined ? null : JSON.stringify(chunk.metadata))}::jsonb)`);
54
+ await this.client.query(`INSERT INTO ${this.table} (id, content, source, metadata) VALUES ${rows.join(', ')}
55
+ ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, source = EXCLUDED.source, metadata = EXCLUDED.metadata`, params.values);
56
+ }
57
+ async delete(ids) {
58
+ if (this.options.shared || ids.length === 0)
59
+ return;
60
+ await this.client.query(`DELETE FROM ${this.table} WHERE id = ANY($1::text[])`, [textArray(ids)]);
61
+ }
62
+ async retrieve(query, options = {}) {
63
+ options.signal?.throwIfAborted();
64
+ const params = new SqlParams();
65
+ const text = params.add(query);
66
+ const language = `'${this.language}'::regconfig`;
67
+ const tsquery = this.options.match === 'all'
68
+ ? `websearch_to_tsquery(${language}, ${text})`
69
+ : `NULLIF(replace(plainto_tsquery(${language}, ${text})::text, ' & ', ' | '), '')::tsquery`;
70
+ const conditions = ['fts @@ q.query'];
71
+ if (options.filter && Object.keys(options.filter).length > 0) {
72
+ conditions.push(`metadata @> ${params.add(JSON.stringify(options.filter))}::jsonb`);
73
+ }
74
+ const limit = params.add(options.topK ?? 5);
75
+ const { rows } = await this.client.query(`SELECT id, content, source, metadata::text AS metadata, ts_rank_cd(fts, q.query, 32) AS score
76
+ FROM ${this.table}, (SELECT ${tsquery} AS query) AS q
77
+ WHERE ${conditions.join(' AND ')}
78
+ ORDER BY score DESC, id LIMIT ${limit}`, params.values);
79
+ return rows.map((row) => ({
80
+ id: row.id,
81
+ content: row.content,
82
+ ...(row.source === null ? {} : { source: row.source }),
83
+ ...(row.metadata === null ? {} : { metadata: fromJson(row.metadata) }),
84
+ score: Number(row.score),
85
+ }));
86
+ }
87
+ async size() {
88
+ const { rows } = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table}`);
89
+ return Number(rows[0]?.count ?? 0);
90
+ }
91
+ }
@@ -38,20 +38,24 @@ export class FilePromptStore {
38
38
  }
39
39
  async setLabel(label, expected) {
40
40
  const file = this.labelFile(label.name, label.label);
41
- if (expected !== undefined) {
42
- const current = await readJson(file);
43
- if (expected === null ? current !== undefined : current?.version !== expected)
44
- return false;
45
- }
46
- await writeJson(file, label);
47
- return true;
41
+ return serialized(file, async () => {
42
+ if (expected !== undefined) {
43
+ const current = await readJson(file);
44
+ if (expected === null ? current !== undefined : current?.version !== expected)
45
+ return false;
46
+ }
47
+ await writeJson(file, label);
48
+ return true;
49
+ });
48
50
  }
49
51
  async deleteLabel(name, label) {
50
52
  const file = this.labelFile(name, label);
51
- if ((await readJson(file)) === undefined)
52
- return false;
53
- await rm(file, { force: true });
54
- return true;
53
+ return serialized(file, async () => {
54
+ if ((await readJson(file)) === undefined)
55
+ return false;
56
+ await rm(file, { force: true });
57
+ return true;
58
+ });
55
59
  }
56
60
  async appendHistory(entry) {
57
61
  await mkdir(this.promptDir(entry.name), { recursive: true });
@@ -109,6 +113,19 @@ async function readJson(file) {
109
113
  return undefined;
110
114
  }
111
115
  }
116
+ const labelWrites = new Map();
117
+ async function serialized(file, work) {
118
+ const key = path.resolve(file);
119
+ const next = (labelWrites.get(key) ?? Promise.resolve()).catch(() => undefined).then(work);
120
+ labelWrites.set(key, next);
121
+ try {
122
+ return await next;
123
+ }
124
+ finally {
125
+ if (labelWrites.get(key) === next)
126
+ labelWrites.delete(key);
127
+ }
128
+ }
112
129
  async function writeJson(file, value) {
113
130
  await mkdir(path.dirname(file), { recursive: true });
114
131
  const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;