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.
- package/API_STABILITY.md +225 -0
- package/CHANGELOG.md +325 -1
- package/README.md +20 -6
- package/SECURITY.md +140 -0
- package/dist/adapter-kit/contracts.d.ts +87 -0
- package/dist/adapter-kit/contracts.js +237 -0
- package/dist/adapter-kit/errors.d.ts +39 -0
- package/dist/adapter-kit/errors.js +52 -0
- package/dist/adapter-kit/index.d.ts +144 -0
- package/dist/adapter-kit/index.js +178 -0
- package/dist/adapter-kit/semver.d.ts +12 -0
- package/dist/adapter-kit/semver.js +55 -0
- package/dist/agent/capabilities.d.ts +39 -0
- package/dist/agent/capabilities.js +36 -0
- package/dist/agent/create-agent.d.ts +61 -11
- package/dist/agent/create-agent.js +136 -22
- package/dist/agent/index.d.ts +3 -2
- package/dist/agent/index.js +1 -1
- package/dist/agent/middleware/approval.d.ts +26 -0
- package/dist/agent/middleware/approval.js +23 -0
- package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist/agent/middleware/context-editor.d.ts +52 -0
- package/dist/agent/middleware/context-editor.js +66 -0
- package/dist/agent/middleware/filesystem.d.ts +44 -0
- package/dist/agent/middleware/filesystem.js +83 -0
- package/dist/agent/middleware/index.d.ts +19 -0
- package/dist/agent/middleware/index.js +9 -0
- package/dist/agent/middleware/limits.d.ts +42 -0
- package/dist/agent/middleware/limits.js +52 -0
- package/dist/agent/middleware/models.d.ts +48 -0
- package/dist/agent/middleware/models.js +44 -0
- package/dist/agent/middleware/pii.d.ts +67 -0
- package/dist/agent/middleware/pii.js +142 -0
- package/dist/agent/middleware/retry.d.ts +66 -0
- package/dist/agent/middleware/retry.js +59 -0
- package/dist/agent/middleware/shared.d.ts +41 -0
- package/dist/agent/middleware/shared.js +73 -0
- package/dist/agent/middleware/tool-selector.d.ts +64 -0
- package/dist/agent/middleware/tool-selector.js +106 -0
- package/dist/agent/permissions.d.ts +113 -0
- package/dist/agent/permissions.js +180 -0
- package/dist/agent/sandbox.d.ts +151 -0
- package/dist/agent/sandbox.js +257 -0
- package/dist/agent/tool.d.ts +8 -3
- package/dist/agent/tool.js +14 -3
- package/dist/core/secure-stream.js +4 -3
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/embeddings/adapters.js +2 -1
- package/dist/evals/judge.js +2 -5
- package/dist/evaluate/cache.d.ts +0 -14
- package/dist/evaluate/cache.js +0 -30
- package/dist/evaluate/file-cache.d.ts +15 -0
- package/dist/evaluate/file-cache.js +30 -0
- package/dist/evaluate/index.d.ts +2 -1
- package/dist/evaluate/index.js +2 -1
- package/dist/evaluate/review.js +2 -2
- package/dist/evaluate/run.js +2 -2
- package/dist/evaluate/version.js +3 -3
- package/dist/graph/functional.d.ts +9 -0
- package/dist/graph/functional.js +20 -3
- package/dist/graph/graph.js +27 -6
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/graph/node-cache.js +3 -3
- package/dist/hallucination/rag.d.ts +4 -1
- package/dist/hallucination/rag.js +6 -1
- package/dist/images/asset-stores.js +2 -3
- package/dist/images/asset-support.js +2 -2
- package/dist/images/assets.js +1 -2
- package/dist/images/evals.js +4 -4
- package/dist/insights/proposals.js +1 -2
- package/dist/insights/regressions.d.ts +26 -2
- package/dist/insights/regressions.js +129 -3
- package/dist/loaders/git.js +8 -1
- package/dist/loaders/web.js +7 -1
- package/dist/mcp/client.d.ts +14 -0
- package/dist/mcp/client.js +59 -1
- package/dist/mcp/registry.d.ts +6 -0
- package/dist/mcp/registry.js +7 -0
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/prompts/file.js +28 -11
- package/dist/protocols/a2a.d.ts +235 -0
- package/dist/protocols/a2a.js +386 -0
- package/dist/protocols/acp.d.ts +87 -0
- package/dist/protocols/acp.js +322 -0
- package/dist/protocols/ag-ui.d.ts +120 -0
- package/dist/protocols/ag-ui.js +180 -0
- package/dist/protocols/shared.d.ts +80 -0
- package/dist/protocols/shared.js +49 -0
- package/dist/providers/anthropic.js +1 -0
- package/dist/providers/cohere.js +2 -2
- package/dist/providers/google.js +2 -11
- package/dist/providers/openai.js +1 -0
- package/dist/rag/elasticsearch.d.ts +71 -0
- package/dist/rag/elasticsearch.js +130 -0
- package/dist/rag/file-ingestion.js +15 -9
- package/dist/rag/pipeline.d.ts +140 -0
- package/dist/rag/pipeline.js +187 -0
- package/dist/rag/rerankers.d.ts +92 -0
- package/dist/rag/rerankers.js +97 -0
- package/dist/rag/retrievers.d.ts +12 -1
- package/dist/runtime/detect.d.ts +17 -0
- package/dist/runtime/detect.js +16 -0
- package/dist/runtime/index.d.ts +19 -0
- package/dist/runtime/index.js +7 -0
- package/dist/security/injection-detector.d.ts +2 -0
- package/dist/security/injection-detector.js +7 -1
- package/dist/security/input-guard.js +5 -2
- package/dist/security/output-guard.js +8 -4
- package/dist/security/pii-detector.js +22 -6
- package/dist/security/prompt-hardening.js +6 -2
- package/dist/security/semantic-injection-classifier.d.ts +2 -1
- package/dist/security/semantic-injection-classifier.js +31 -35
- package/dist/security/upload-scanner.d.ts +8 -0
- package/dist/security/upload-scanner.js +51 -9
- package/dist/server/assistant.d.ts +5 -1
- package/dist/server/assistant.js +4 -2
- package/dist/server/auth.d.ts +250 -0
- package/dist/server/auth.js +392 -0
- package/dist/server/deployments.d.ts +63 -1
- package/dist/server/deployments.js +75 -2
- package/dist/server/runs.d.ts +9 -0
- package/dist/server/runs.js +20 -0
- package/dist/server/server.js +5 -0
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/testing/record.js +2 -2
- package/dist/tracing/stores.js +2 -1
- package/dist/tracing/tracer.js +2 -2
- package/dist/types/config.d.ts +20 -0
- package/dist/types/graph.d.ts +38 -0
- package/dist/types/messages.d.ts +36 -2
- package/dist/types/principal.d.ts +21 -0
- package/dist/types/principal.js +1 -0
- package/dist/types/server.d.ts +8 -9
- package/dist/utils/base64.d.ts +4 -0
- package/dist/utils/base64.js +21 -0
- package/dist/utils/checksums.d.ts +22 -0
- package/dist/utils/checksums.js +39 -0
- package/dist/utils/ids.d.ts +8 -0
- package/dist/utils/ids.js +26 -2
- package/dist/utils/sha256.d.ts +22 -0
- package/dist/utils/sha256.js +120 -0
- package/dist/utils/tenant.d.ts +11 -0
- package/dist/utils/tenant.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/adapter-kit/contracts.d.ts +87 -0
- package/dist-cjs/adapter-kit/contracts.js +243 -0
- package/dist-cjs/adapter-kit/errors.d.ts +39 -0
- package/dist-cjs/adapter-kit/errors.js +57 -0
- package/dist-cjs/adapter-kit/index.d.ts +144 -0
- package/dist-cjs/adapter-kit/index.js +194 -0
- package/dist-cjs/adapter-kit/semver.d.ts +12 -0
- package/dist-cjs/adapter-kit/semver.js +59 -0
- package/dist-cjs/agent/capabilities.d.ts +39 -0
- package/dist-cjs/agent/capabilities.js +41 -0
- package/dist-cjs/agent/create-agent.d.ts +61 -11
- package/dist-cjs/agent/create-agent.js +136 -22
- package/dist-cjs/agent/index.d.ts +3 -2
- package/dist-cjs/agent/index.js +4 -4
- package/dist-cjs/agent/middleware/approval.d.ts +26 -0
- package/dist-cjs/agent/middleware/approval.js +26 -0
- package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
- package/dist-cjs/agent/middleware/context-editor.js +70 -0
- package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
- package/dist-cjs/agent/middleware/filesystem.js +86 -0
- package/dist-cjs/agent/middleware/index.d.ts +19 -0
- package/dist-cjs/agent/middleware/index.js +28 -0
- package/dist-cjs/agent/middleware/limits.d.ts +42 -0
- package/dist-cjs/agent/middleware/limits.js +57 -0
- package/dist-cjs/agent/middleware/models.d.ts +48 -0
- package/dist-cjs/agent/middleware/models.js +48 -0
- package/dist-cjs/agent/middleware/pii.d.ts +67 -0
- package/dist-cjs/agent/middleware/pii.js +147 -0
- package/dist-cjs/agent/middleware/retry.d.ts +66 -0
- package/dist-cjs/agent/middleware/retry.js +63 -0
- package/dist-cjs/agent/middleware/shared.d.ts +41 -0
- package/dist-cjs/agent/middleware/shared.js +83 -0
- package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
- package/dist-cjs/agent/middleware/tool-selector.js +109 -0
- package/dist-cjs/agent/permissions.d.ts +113 -0
- package/dist-cjs/agent/permissions.js +189 -0
- package/dist-cjs/agent/sandbox.d.ts +151 -0
- package/dist-cjs/agent/sandbox.js +297 -0
- package/dist-cjs/agent/tool.d.ts +8 -3
- package/dist-cjs/agent/tool.js +14 -3
- package/dist-cjs/core/secure-stream.js +4 -3
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/embeddings/adapters.js +2 -1
- package/dist-cjs/evals/judge.js +2 -5
- package/dist-cjs/evaluate/cache.d.ts +0 -14
- package/dist-cjs/evaluate/cache.js +1 -65
- package/dist-cjs/evaluate/file-cache.d.ts +15 -0
- package/dist-cjs/evaluate/file-cache.js +67 -0
- package/dist-cjs/evaluate/index.d.ts +2 -1
- package/dist-cjs/evaluate/index.js +3 -2
- package/dist-cjs/evaluate/review.js +2 -2
- package/dist-cjs/evaluate/run.js +2 -2
- package/dist-cjs/evaluate/version.js +3 -3
- package/dist-cjs/graph/functional.d.ts +9 -0
- package/dist-cjs/graph/functional.js +20 -3
- package/dist-cjs/graph/graph.js +27 -6
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/graph/node-cache.js +3 -3
- package/dist-cjs/hallucination/rag.d.ts +4 -1
- package/dist-cjs/hallucination/rag.js +6 -1
- package/dist-cjs/images/asset-stores.js +2 -3
- package/dist-cjs/images/asset-support.js +2 -2
- package/dist-cjs/images/assets.js +1 -2
- package/dist-cjs/images/evals.js +4 -4
- package/dist-cjs/insights/proposals.js +1 -2
- package/dist-cjs/insights/regressions.d.ts +26 -2
- package/dist-cjs/insights/regressions.js +129 -3
- package/dist-cjs/loaders/git.js +8 -1
- package/dist-cjs/loaders/web.js +7 -1
- package/dist-cjs/mcp/client.d.ts +14 -0
- package/dist-cjs/mcp/client.js +60 -1
- package/dist-cjs/mcp/registry.d.ts +6 -0
- package/dist-cjs/mcp/registry.js +7 -0
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/prompts/file.js +28 -11
- package/dist-cjs/protocols/a2a.d.ts +235 -0
- package/dist-cjs/protocols/a2a.js +393 -0
- package/dist-cjs/protocols/acp.d.ts +87 -0
- package/dist-cjs/protocols/acp.js +326 -0
- package/dist-cjs/protocols/ag-ui.d.ts +120 -0
- package/dist-cjs/protocols/ag-ui.js +186 -0
- package/dist-cjs/protocols/shared.d.ts +80 -0
- package/dist-cjs/protocols/shared.js +59 -0
- package/dist-cjs/providers/anthropic.js +1 -0
- package/dist-cjs/providers/cohere.js +2 -2
- package/dist-cjs/providers/google.js +3 -12
- package/dist-cjs/providers/openai.js +1 -0
- package/dist-cjs/rag/elasticsearch.d.ts +71 -0
- package/dist-cjs/rag/elasticsearch.js +135 -0
- package/dist-cjs/rag/file-ingestion.js +14 -8
- package/dist-cjs/rag/pipeline.d.ts +140 -0
- package/dist-cjs/rag/pipeline.js +191 -0
- package/dist-cjs/rag/rerankers.d.ts +92 -0
- package/dist-cjs/rag/rerankers.js +107 -0
- package/dist-cjs/rag/retrievers.d.ts +12 -1
- package/dist-cjs/runtime/detect.d.ts +17 -0
- package/dist-cjs/runtime/detect.js +19 -0
- package/dist-cjs/runtime/index.d.ts +19 -0
- package/dist-cjs/runtime/index.js +31 -0
- package/dist-cjs/security/injection-detector.d.ts +2 -0
- package/dist-cjs/security/injection-detector.js +7 -1
- package/dist-cjs/security/input-guard.js +5 -2
- package/dist-cjs/security/output-guard.js +8 -4
- package/dist-cjs/security/pii-detector.js +22 -6
- package/dist-cjs/security/prompt-hardening.js +6 -2
- package/dist-cjs/security/semantic-injection-classifier.d.ts +2 -1
- package/dist-cjs/security/semantic-injection-classifier.js +31 -35
- package/dist-cjs/security/upload-scanner.d.ts +8 -0
- package/dist-cjs/security/upload-scanner.js +53 -9
- package/dist-cjs/server/assistant.d.ts +5 -1
- package/dist-cjs/server/assistant.js +4 -2
- package/dist-cjs/server/auth.d.ts +250 -0
- package/dist-cjs/server/auth.js +402 -0
- package/dist-cjs/server/deployments.d.ts +63 -1
- package/dist-cjs/server/deployments.js +75 -2
- package/dist-cjs/server/runs.d.ts +9 -0
- package/dist-cjs/server/runs.js +20 -0
- package/dist-cjs/server/server.js +5 -0
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/testing/record.js +2 -2
- package/dist-cjs/tracing/stores.js +2 -1
- package/dist-cjs/tracing/tracer.js +2 -2
- package/dist-cjs/types/config.d.ts +20 -0
- package/dist-cjs/types/graph.d.ts +38 -0
- package/dist-cjs/types/messages.d.ts +36 -2
- package/dist-cjs/types/principal.d.ts +21 -0
- package/dist-cjs/types/principal.js +2 -0
- package/dist-cjs/types/server.d.ts +8 -9
- package/dist-cjs/utils/base64.d.ts +4 -0
- package/dist-cjs/utils/base64.js +25 -0
- package/dist-cjs/utils/checksums.d.ts +22 -0
- package/dist-cjs/utils/checksums.js +46 -0
- package/dist-cjs/utils/ids.d.ts +8 -0
- package/dist-cjs/utils/ids.js +28 -2
- package/dist-cjs/utils/sha256.d.ts +22 -0
- package/dist-cjs/utils/sha256.js +125 -0
- package/dist-cjs/utils/tenant.d.ts +11 -0
- package/dist-cjs/utils/tenant.js +15 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +7 -3
- package/package.json +193 -6
package/README.md
CHANGED
|
@@ -8,7 +8,10 @@ team can share.
|
|
|
8
8
|
Import only what you use. The root import is the client and nothing else, and every other capability
|
|
9
9
|
has its own entry point with a size budget CI enforces; [the packaging guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md)
|
|
10
10
|
publishes what each one costs. The package has no required dependencies: a graph-only application
|
|
11
|
-
loads
|
|
11
|
+
loads 81 KB, and installs nothing but this package.
|
|
12
|
+
The client, the provider adapters, graphs, agents, and the agent protocols run on Node.js, Deno,
|
|
13
|
+
Bun, edge runtimes, and browsers; [the runtimes guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/runtimes.md)
|
|
14
|
+
lists the entry points that need Node.js, and why.
|
|
12
15
|
|
|
13
16
|
- NPM: https://www.npmjs.com/package/nexus-ai-pro
|
|
14
17
|
- GitHub: https://github.com/mkhitar-abrahamyan/nexus-ai
|
|
@@ -132,9 +135,11 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
132
135
|
| [Providers and routing](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/providers.md) | The twelve completion providers, routing and failover, the model registry |
|
|
133
136
|
| [Guardrails](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/security.md) | Injection, PII, secrets, schema validation, output redaction |
|
|
134
137
|
| [Graphs](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/graphs.md) | Parallel branches, interrupts, checkpoints, time travel, subgraphs, caching, diagrams |
|
|
135
|
-
| [Agents and tools](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/agents.md) | Graph-based agents with approvals and middleware, and the simple tool loop |
|
|
138
|
+
| [Agents and tools](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/agents.md) | Graph-based agents with approvals, capabilities and permission policies, a sandbox contract, the middleware catalog, and the simple tool loop |
|
|
139
|
+
| [The deep-agent preset](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deep-agents.md) | Long, multi-step agents: a plan, a sandboxed workspace, helpers, skills, and a context that stays small |
|
|
136
140
|
| [Long-term memory](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/memory.md) | Namespaced memory with semantic search, in memory or Redis |
|
|
137
141
|
| [MCP](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/mcp.md) | Borrowing tools from MCP servers, a registry of many, and serving your own |
|
|
142
|
+
| [Agent protocols](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/protocols.md) | Serving an agent to user interfaces over AG-UI, to other agents over A2A, and to code editors over ACP, and calling remote agents as tools |
|
|
138
143
|
| [Traces](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/tracing.md) | Run trees, queries, feedback, comparison, and alerts |
|
|
139
144
|
| [Evaluation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/evaluation.md) | Datasets, evaluators, experiments, comparisons with a verdict, review queues, LLM judges |
|
|
140
145
|
| [Context hub](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/context-hub.md) | Prompts, instructions, tools, and skills versioned together, promoted through gates, and moved between projects |
|
|
@@ -155,6 +160,7 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
155
160
|
| [Realtime voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/realtime.md) | Browser and server realtime sessions with barge-in, tools, and exports |
|
|
156
161
|
| [Telephony](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/telephony.md) | Calls, webhooks, phone numbers, and phone agents on realtime sessions |
|
|
157
162
|
| [Testing](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/testing.md) | Recording and replaying provider traffic, and conformance suites |
|
|
163
|
+
| [Adapter kit](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/adapter-kit.md) | Building a provider, store, or retriever outside this repository, and verifying it against the same contracts |
|
|
158
164
|
| [Studio](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/studio.md) | A shared UI for traces, threads, approvals, experiments, prompts, deployments, costs, and health, as a separate package |
|
|
159
165
|
| [Agent server](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/server.md) | Self-hosted HTTP server for assistants: threads, durable runs, resumable streams, cron, a worker queue, and metrics |
|
|
160
166
|
| [Deployments](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deployments.md) | Revisions and canaries with automatic rollback, autoscaling on Kubernetes and Helm, and per-tenant limits |
|
|
@@ -162,6 +168,7 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
162
168
|
| [Workflows](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/workflows.md) | Ready-made chains and domain workflows |
|
|
163
169
|
| [Command line](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/cli.md) | The `nexus` command |
|
|
164
170
|
| [Packaging and install weight](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md) | Every entry point and what it costs |
|
|
171
|
+
| [Runtimes](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/runtimes.md) | The portable kernel, and running the client, providers, graphs, and agents on Deno, Bun, edge runtimes, and browsers |
|
|
165
172
|
|
|
166
173
|
## Examples
|
|
167
174
|
|
|
@@ -251,13 +258,20 @@ Shipped so far:
|
|
|
251
258
|
backend, one event stream, and dashboard rollups;
|
|
252
259
|
- 2.2 production scale and safe upgrades: versioned database migrations, queue claims and routing
|
|
253
260
|
that cost the same at any size, a circuit breaker that counts across workers, one tenant model
|
|
254
|
-
across every store, and `nexus doctor` to check a deployment before it serves traffic
|
|
261
|
+
across every store, and `nexus doctor` to check a deployment before it serves traffic;
|
|
262
|
+
- 2.3 agents that act safely, on production retrieval: capabilities and permission policies, a
|
|
263
|
+
sandbox contract, a middleware catalog, server authentication with one principal on every run,
|
|
264
|
+
keyword search in Postgres and Elasticsearch with rerankers, durable ingestion, and a deep-agent
|
|
265
|
+
preset;
|
|
266
|
+
- 2.4 graduation and interoperability: SQLite, loaders, retrievers, the MCP registry, the context
|
|
267
|
+
hub, insights, and tenant limits out of experimental with published evidence; canaries judged with
|
|
268
|
+
confidence and on quality; an adapter kit; AG-UI, A2A, and ACP; and a portable kernel, with the
|
|
269
|
+
client, providers, graphs, and agents running on Deno, Bun, edge runtimes, and browsers.
|
|
255
270
|
|
|
256
271
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
257
272
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
releases after it.
|
|
273
|
+
What comes next is on the [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md):
|
|
274
|
+
the gaps 2.4 left open, and the work applications ask for after them.
|
|
261
275
|
|
|
262
276
|
## Before Production
|
|
263
277
|
|
package/SECURITY.md
CHANGED
|
@@ -44,3 +44,143 @@ Nexus guardrails reduce common AI application risks, but they are not a complete
|
|
|
44
44
|
- Browser WebRTC still requires a backend security boundary. A direct media path reduces latency; it
|
|
45
45
|
does not remove the need for server-owned credentials, session policy, tool authorization, abuse
|
|
46
46
|
monitoring, and provider-side safety controls.
|
|
47
|
+
|
|
48
|
+
## Threat reviews
|
|
49
|
+
|
|
50
|
+
Every surface that leaves experimental status is reviewed for how untrusted input reaches it and
|
|
51
|
+
what it could do with that input. Each review is published here with its findings, which are closed
|
|
52
|
+
before the surface graduates. A finding is closed in one of two ways: by a fix with a test that
|
|
53
|
+
names the attack, or by a documented limitation that the surface's guide states.
|
|
54
|
+
|
|
55
|
+
These reviews were done for 2.4.
|
|
56
|
+
|
|
57
|
+
### SQLite adapters
|
|
58
|
+
|
|
59
|
+
Trust boundary: an application's data, an operation's payload, and metadata filters that may come
|
|
60
|
+
from a request.
|
|
61
|
+
- Table names are checked against a strict pattern and quoted. Every value, including each metadata
|
|
62
|
+
filter key and its JSON path, is a bound parameter, so a filter cannot change a statement.
|
|
63
|
+
- A migration records its checksum, and a changed migration is refused rather than reapplied.
|
|
64
|
+
- Every change to an operation is one `UPDATE … WHERE sequence = expected`, so two workers sharing
|
|
65
|
+
a file never both claim it. The ten-worker race tests run on SQLite.
|
|
66
|
+
- Tenant data is scoped by key prefix, and the isolation suite runs on every SQLite store.
|
|
67
|
+
|
|
68
|
+
Findings: none.
|
|
69
|
+
|
|
70
|
+
### The Postgres and SQLite vector stores
|
|
71
|
+
|
|
72
|
+
Trust boundary: metadata filters, which applications often build from a request.
|
|
73
|
+
- The pgvector store sends a filter as one bound `jsonb` containment parameter, and quotes its table
|
|
74
|
+
and index names.
|
|
75
|
+
- The SQLite store binds each key's JSON path and value, and matches a value's JSON type as well as
|
|
76
|
+
its value, so `"1"` never matches `1`.
|
|
77
|
+
|
|
78
|
+
Findings: none.
|
|
79
|
+
|
|
80
|
+
### Loaders
|
|
81
|
+
|
|
82
|
+
Trust boundary: everything a loader reads is untrusted. A repository, a website, a sitemap, or an
|
|
83
|
+
uploaded file may be written by an attacker who wants it in a knowledge base, or who wants the
|
|
84
|
+
loader to read something else.
|
|
85
|
+
- Web pages and sitemaps go through the SSRF-safe fetch. Private, loopback, and cloud-metadata
|
|
86
|
+
addresses are refused, every redirect is checked again, and bodies are capped.
|
|
87
|
+
- The Git loader runs `git` without a shell. It disables the `ext::` transport, puts `--` before
|
|
88
|
+
the repository, and refuses a repository that starts with `-`.
|
|
89
|
+
- `loadDirectory()` does not follow symbolic links.
|
|
90
|
+
|
|
91
|
+
Findings, both fixed in 2.4:
|
|
92
|
+
- **The Git loader followed symbolic links.** A tracked link such as `notes.md` pointing to
|
|
93
|
+
`~/.ssh/id_rsa` was read and ingested, so a knowledge base could quote a file of the host. A
|
|
94
|
+
link is now never followed, nor a path that resolves outside the checkout. Proof: a repository
|
|
95
|
+
holding a tracked link to a file outside it loads without that file, cloned and in place. On
|
|
96
|
+
Linux, the test fails on the earlier loader.
|
|
97
|
+
- **A gzipped sitemap could be a decompression bomb.** A few megabytes of gzip inflated without a
|
|
98
|
+
limit. Inflation now stops at 50 MB, the sitemap protocol's own maximum. Proof: 60 MB of gzip
|
|
99
|
+
that inflates past it is refused.
|
|
100
|
+
|
|
101
|
+
### Retrievers
|
|
102
|
+
|
|
103
|
+
Trust boundary: queries come from users, and documents may hold text written to steer a model.
|
|
104
|
+
- Tokenizing is one linear regular expression, with no backtracking on hostile input.
|
|
105
|
+
- Filters compare values; they never become code or a query language.
|
|
106
|
+
- A model reranker or query-variant generator reads documents and queries as data in its prompt. Its
|
|
107
|
+
output is parsed as JSON and used only to order or widen a search, never to call a tool.
|
|
108
|
+
|
|
109
|
+
Findings: none.
|
|
110
|
+
|
|
111
|
+
### MCP registry
|
|
112
|
+
|
|
113
|
+
Trust boundary: the configuration file is trusted, since it names programs to run. What a server
|
|
114
|
+
says about itself is not: its tool names, its descriptions, and its results.
|
|
115
|
+
- A server's tools are prefixed with its name by default, so one server cannot shadow another's
|
|
116
|
+
tool.
|
|
117
|
+
- Allow and deny lists limit what agents see, and deny wins.
|
|
118
|
+
- A tool from a server declares no capabilities. A permission policy denies such a tool unless its
|
|
119
|
+
`undeclared` rule says otherwise, and `humanApproval()` asks about it by default.
|
|
120
|
+
- `${NAME}` placeholders keep credentials out of the file. A missing variable is named in the error,
|
|
121
|
+
never its value.
|
|
122
|
+
|
|
123
|
+
Finding, fixed in 2.4: **a local server inherited this process's whole environment.** Any
|
|
124
|
+
`npx`-started server, which is usually third-party code, saw every API key and database URL the
|
|
125
|
+
application held. A server now starts with `env` and only what a process needs to run: the path,
|
|
126
|
+
the home and temporary directories, the locale, proxies, and certificates. The new `inheritEnv`
|
|
127
|
+
option widens this by name, or entirely. Proof: a real child process started by the transport sees
|
|
128
|
+
the variables it was given, and not a secret set in the parent.
|
|
129
|
+
|
|
130
|
+
### Context hub
|
|
131
|
+
|
|
132
|
+
Trust boundary: an imported bundle may come from another team or another organization. Who may
|
|
133
|
+
commit and promote is the application's access control, which the studio's roles enforce.
|
|
134
|
+
- An import recomputes each bundle's and each prompt's content version, and refuses one whose content
|
|
135
|
+
changed after export.
|
|
136
|
+
- A bundle names tools, but `bindTools()` binds only implementations the application supplies, so
|
|
137
|
+
a bundle cannot add code.
|
|
138
|
+
- Promotion runs the label's gates, an experiment over the exact version among them.
|
|
139
|
+
- Labels move by compare-and-set, so a promotion decided on a stale version is refused.
|
|
140
|
+
|
|
141
|
+
Finding, fixed in 2.4: **the file store's compare-and-set could lose a move.** Two label moves at
|
|
142
|
+
once in one process could both pass the check, and the later write erased the earlier one. Label
|
|
143
|
+
writes to one file are now serialized across the process. Proof: two hubs whose writes are held
|
|
144
|
+
until both arrive race for one label on every backend, and on the file store the earlier code
|
|
145
|
+
loses a move every time. Several processes promoting the same label at once still need the Redis or
|
|
146
|
+
Postgres store, as the prompts guide says.
|
|
147
|
+
|
|
148
|
+
### Insights
|
|
149
|
+
|
|
150
|
+
Trust boundary: runs and their errors carry user data, and a model proposes fixes.
|
|
151
|
+
- An error signature replaces numbers, quoted values, URLs, ids, and long hexadecimal strings, so a
|
|
152
|
+
cluster's summary does not carry a customer's data.
|
|
153
|
+
- A model's proposed fix is parsed as data, evaluated on a dataset, and never applied without a
|
|
154
|
+
person. A promotion still runs the label's gates.
|
|
155
|
+
- A proposal id is checked against a strict pattern before it becomes a file name.
|
|
156
|
+
|
|
157
|
+
Findings: none.
|
|
158
|
+
|
|
159
|
+
### Tenant limits and the worker queue
|
|
160
|
+
|
|
161
|
+
Trust boundary: a tenant is whoever the application's `authenticate()` says, never a header the
|
|
162
|
+
server trusts by itself.
|
|
163
|
+
- Concurrency, rate, and budget limits are kept in a shared store, so several replicas enforce one
|
|
164
|
+
limit. Admission and release are tested by ten racing workers.
|
|
165
|
+
- A run is released exactly once, however it ends.
|
|
166
|
+
- The queue claims an operation under a lease in one atomic step on every store. A lost lease
|
|
167
|
+
cancels the run's signal, and a crash at any durable boundary resumes to the same state.
|
|
168
|
+
|
|
169
|
+
Findings: none.
|
|
170
|
+
|
|
171
|
+
### Deployments (reviewed; still experimental)
|
|
172
|
+
|
|
173
|
+
Findings:
|
|
174
|
+
- **Fixed in 2.4: one caller's ratings could decide a canary.** A canary was judged on feedback
|
|
175
|
+
entries, not on runs. A hundred low ratings of one run met the minimum sample alone, and could
|
|
176
|
+
roll back a canary for everyone, or keep a bad one alive. Each run now counts once per feedback
|
|
177
|
+
key, as the mean of its ratings.
|
|
178
|
+
- **Fixed in 2.4: two changes at once on one replica could lose one**, such as a guard's rollback
|
|
179
|
+
and an operator's promotion. Changes to one deployment now apply in order.
|
|
180
|
+
- **Open: two replicas changing one deployment in the same instant can lose one change.** The
|
|
181
|
+
server's state store has no atomic compare-and-set. Until it does, deployments stay experimental.
|
|
182
|
+
The guide says to make changes from one place, or to pass `expectedVersion` and retry a conflict.
|
|
183
|
+
- **Documented limitation: `bucket()` spreads sequential ids unevenly.** Ids that differ only in their
|
|
184
|
+
last characters spread unevenly over a few hundred threads. Random ids, which the server gives by
|
|
185
|
+
default, spread evenly. The function's output is part of the stability promise, so it is not
|
|
186
|
+
changed in 2.x.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract suites the adapter kit ships: what every vector store, retriever, and long-term store
|
|
3
|
+
* must do, as checks that need no test framework. Each check is named for the behavior it proves,
|
|
4
|
+
* and a capability the adapter does not declare is reported as skipped, never as a failure.
|
|
5
|
+
*/
|
|
6
|
+
import type { RagChunk } from '../hallucination/rag.js';
|
|
7
|
+
import type { EmbeddingProvider, VectorStore } from '../hallucination/retrieval.js';
|
|
8
|
+
import type { Retriever } from '../rag/retrievers.js';
|
|
9
|
+
import type { Store } from '../types/store.js';
|
|
10
|
+
/** One check of a contract suite. */
|
|
11
|
+
export interface AdapterCheck {
|
|
12
|
+
/** What was checked, as a sentence. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Whether it held. */
|
|
15
|
+
ok: boolean;
|
|
16
|
+
/** Why it did not hold, or why it was skipped. */
|
|
17
|
+
detail?: string;
|
|
18
|
+
/** True when the adapter does not declare the capability the check needs. */
|
|
19
|
+
skipped?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The passages every retrieval contract runs on, and that benchmarks reuse. */
|
|
22
|
+
export declare const ADAPTER_FIXTURE_PASSAGES: readonly RagChunk[];
|
|
23
|
+
/** What a vector store declares it supports, for its contract. */
|
|
24
|
+
export interface VectorStoreCapabilities {
|
|
25
|
+
/** `searchVector()` with a vector computed elsewhere. Defaults to true. */
|
|
26
|
+
searchVector?: boolean;
|
|
27
|
+
/** `minScore` drops weak matches. Defaults to true. */
|
|
28
|
+
minScore?: boolean;
|
|
29
|
+
/** Metadata filters: `exact` matches values, `typed` also tells `3` from `'3'`, `none` has no filters. Defaults to `exact`. */
|
|
30
|
+
filters?: 'none' | 'exact' | 'typed';
|
|
31
|
+
}
|
|
32
|
+
/** Options for `runVectorStoreContract()`. */
|
|
33
|
+
export interface VectorStoreContractOptions {
|
|
34
|
+
/** The embedding function the store was given, to compute a query vector for `searchVector()`. */
|
|
35
|
+
embed: EmbeddingProvider;
|
|
36
|
+
/** What the store supports. */
|
|
37
|
+
capabilities?: VectorStoreCapabilities;
|
|
38
|
+
/** Waits for writes to become searchable, for a store whose index is eventually consistent. */
|
|
39
|
+
settle?: () => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The `VectorStore` contract: ranking, `topK`, sources and metadata round-tripped, filters,
|
|
43
|
+
* `minScore`, vector search, replacement by id, deletes, and empty writes. The store should be empty
|
|
44
|
+
* when it starts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function runVectorStoreContract(store: VectorStore, options: VectorStoreContractOptions): Promise<AdapterCheck[]>;
|
|
47
|
+
/** What a retriever declares it supports, for its contract. */
|
|
48
|
+
export interface RetrieverCapabilities {
|
|
49
|
+
/** It holds its own passages, with `add()` and `delete()`: a `SparseRetriever`. Defaults to whether it has them. */
|
|
50
|
+
writes?: boolean;
|
|
51
|
+
/** Metadata filters. Defaults to true. */
|
|
52
|
+
filters?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Options for `runRetrieverContract()`. */
|
|
55
|
+
export interface RetrieverContractOptions {
|
|
56
|
+
/** What the retriever supports. */
|
|
57
|
+
capabilities?: RetrieverCapabilities;
|
|
58
|
+
/**
|
|
59
|
+
* Loads the fixture passages, for a retriever that does not hold its own: into the vector store
|
|
60
|
+
* behind it, say. A retriever with `add()` is loaded through that.
|
|
61
|
+
*/
|
|
62
|
+
load?: (passages: RagChunk[]) => Promise<void>;
|
|
63
|
+
/** Waits for writes to become searchable. */
|
|
64
|
+
settle?: () => Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `Retriever` contract, and the `SparseRetriever` contract when the retriever takes writes:
|
|
68
|
+
* an exact term found, ranking, `topK`, filters, replacement, deletes, and an empty query.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runRetrieverContract(retriever: Retriever, options?: RetrieverContractOptions): Promise<AdapterCheck[]>;
|
|
71
|
+
/** What a long-term store declares it supports, for its contract. */
|
|
72
|
+
export interface StoreCapabilities {
|
|
73
|
+
/** `search()` lists a namespace prefix, newest first. Defaults to true. */
|
|
74
|
+
search?: boolean;
|
|
75
|
+
/** `search()` filters on value fields. Defaults to true. */
|
|
76
|
+
filters?: boolean;
|
|
77
|
+
/** `listNamespaces()`. Defaults to true. */
|
|
78
|
+
namespaces?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The long-term `Store` contract: values round-trip, a replaced item keeps its creation time, a
|
|
82
|
+
* missing key reads as nothing, deletes, prefix search with filters and paging, and namespace
|
|
83
|
+
* listing. The store should be empty when it starts.
|
|
84
|
+
*/
|
|
85
|
+
export declare function runStoreContract(store: Store, options?: {
|
|
86
|
+
capabilities?: StoreCapabilities;
|
|
87
|
+
}): Promise<AdapterCheck[]>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export const ADAPTER_FIXTURE_PASSAGES = [
|
|
2
|
+
{
|
|
3
|
+
id: 'refunds#0',
|
|
4
|
+
content: 'Refunds are issued within fourteen days of a return',
|
|
5
|
+
source: 'policy.md',
|
|
6
|
+
metadata: { tenant: 'acme', page: 3 },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'shipping#0',
|
|
10
|
+
content: 'Orders ship from the warehouse within two business days',
|
|
11
|
+
source: 'policy.md',
|
|
12
|
+
metadata: { tenant: 'acme' },
|
|
13
|
+
},
|
|
14
|
+
{ id: 'refunds#1', content: 'A refund goes back to the original payment method', metadata: { tenant: 'globex' } },
|
|
15
|
+
{ id: 'careers#0', content: 'We are hiring engineers in three offices' },
|
|
16
|
+
];
|
|
17
|
+
function fixturePassages() {
|
|
18
|
+
return ADAPTER_FIXTURE_PASSAGES.map((passage) => ({
|
|
19
|
+
...passage,
|
|
20
|
+
...(passage.metadata ? { metadata: { ...passage.metadata } } : {}),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
class Suite {
|
|
24
|
+
checks = [];
|
|
25
|
+
async check(name, run) {
|
|
26
|
+
try {
|
|
27
|
+
const result = await run();
|
|
28
|
+
this.checks.push(result === true ? { name, ok: true } : { name, ok: false, detail: result === false ? 'did not hold' : result });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
this.checks.push({ name, ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
skip(name, reason) {
|
|
35
|
+
this.checks.push({ name, ok: true, skipped: true, detail: reason });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function runVectorStoreContract(store, options) {
|
|
39
|
+
const suite = new Suite();
|
|
40
|
+
const settle = options.settle ?? (async () => undefined);
|
|
41
|
+
const capabilities = options.capabilities ?? {};
|
|
42
|
+
const filters = capabilities.filters ?? 'exact';
|
|
43
|
+
await suite.check('adds passages', async () => {
|
|
44
|
+
await store.add(fixturePassages());
|
|
45
|
+
await settle();
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
await suite.check('ranks the closest passage first, best first, within topK', async () => {
|
|
49
|
+
const results = await store.search('how long do refunds take', { topK: 2 });
|
|
50
|
+
if (results.length !== 2)
|
|
51
|
+
return `expected 2 results for topK 2, got ${results.length}`;
|
|
52
|
+
if (results[0]?.id !== 'refunds#0')
|
|
53
|
+
return `expected refunds#0 first, got ${results[0]?.id}`;
|
|
54
|
+
return (results[0]?.score ?? 0) > (results[1]?.score ?? 0) || 'results are not ordered by score';
|
|
55
|
+
});
|
|
56
|
+
await suite.check('returns a passage source and its metadata whole', async () => {
|
|
57
|
+
const [first] = await store.search('how long do refunds take', { topK: 1 });
|
|
58
|
+
if (first?.source !== 'policy.md')
|
|
59
|
+
return `expected source policy.md, got ${first?.source}`;
|
|
60
|
+
return (JSON.stringify(first?.metadata) === JSON.stringify({ tenant: 'acme', page: 3 }) ||
|
|
61
|
+
`metadata came back as ${JSON.stringify(first?.metadata)}`);
|
|
62
|
+
});
|
|
63
|
+
if (filters === 'none')
|
|
64
|
+
suite.skip('narrows a search by metadata', 'the store declares no filters');
|
|
65
|
+
else {
|
|
66
|
+
await suite.check('narrows a search by metadata', async () => {
|
|
67
|
+
const results = await store.search('refund', { filter: { tenant: 'globex' } });
|
|
68
|
+
return ((results.length === 1 && results[0]?.id === 'refunds#1') ||
|
|
69
|
+
`got ${results.map((result) => result.id).join(', ')}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (filters === 'typed') {
|
|
73
|
+
await suite.check('tells a number from the same digits as text in a filter', async () => {
|
|
74
|
+
const numeric = await store.search('refunds', { filter: { page: 3 }, topK: 10 });
|
|
75
|
+
const text = await store.search('refunds', { filter: { page: '3' }, topK: 10 });
|
|
76
|
+
return ((numeric.length === 1 && text.length === 0) || `number matched ${numeric.length}, text matched ${text.length}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
suite.skip('tells a number from the same digits as text in a filter', 'the store declares untyped filters');
|
|
81
|
+
if (capabilities.minScore === false)
|
|
82
|
+
suite.skip('drops matches under minScore', 'the store declares no minScore');
|
|
83
|
+
else {
|
|
84
|
+
await suite.check('drops matches under minScore', async () => {
|
|
85
|
+
const results = await store.search('how long do refunds take', { minScore: 0.99 });
|
|
86
|
+
return results.length === 0 || `${results.length} results above 0.99`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (capabilities.searchVector === false)
|
|
90
|
+
suite.skip('searches by a precomputed vector', 'the store declares no searchVector');
|
|
91
|
+
else {
|
|
92
|
+
await suite.check('searches by a precomputed vector', async () => {
|
|
93
|
+
const [vector] = await options.embed(['orders ship from the warehouse']);
|
|
94
|
+
const [first] = await store.searchVector(vector, { topK: 1 });
|
|
95
|
+
return first?.id === 'shipping#0' || `expected shipping#0, got ${first?.id}`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
await suite.check('replaces a passage added again under its id', async () => {
|
|
99
|
+
await store.add([
|
|
100
|
+
{ id: 'careers#0', content: 'Refunds for event tickets are not available', metadata: { tenant: 'acme' } },
|
|
101
|
+
]);
|
|
102
|
+
await settle();
|
|
103
|
+
const results = await store.search('event tickets refunds', { topK: 10 });
|
|
104
|
+
const copies = results.filter((result) => result.id === 'careers#0');
|
|
105
|
+
if (copies.length !== 1)
|
|
106
|
+
return `found ${copies.length} copies`;
|
|
107
|
+
return /event tickets/.test(copies[0]?.content ?? '') || 'the old content is still there';
|
|
108
|
+
});
|
|
109
|
+
await suite.check('deletes by id, ignoring ids it does not hold', async () => {
|
|
110
|
+
await store.delete(['refunds#0', 'not-stored']);
|
|
111
|
+
await settle();
|
|
112
|
+
const results = await store.search('how long do refunds take', { topK: 10 });
|
|
113
|
+
return !results.some((result) => result.id === 'refunds#0') || 'the deleted passage is still found';
|
|
114
|
+
});
|
|
115
|
+
await suite.check('accepts empty writes', async () => {
|
|
116
|
+
await store.add([]);
|
|
117
|
+
await store.delete([]);
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
return suite.checks;
|
|
121
|
+
}
|
|
122
|
+
export async function runRetrieverContract(retriever, options = {}) {
|
|
123
|
+
const suite = new Suite();
|
|
124
|
+
const settle = options.settle ?? (async () => undefined);
|
|
125
|
+
const sparse = retriever;
|
|
126
|
+
const writes = options.capabilities?.writes ?? (typeof sparse.add === 'function' && typeof sparse.delete === 'function');
|
|
127
|
+
const passages = fixturePassages();
|
|
128
|
+
await suite.check('loads the fixture passages', async () => {
|
|
129
|
+
if (writes)
|
|
130
|
+
await sparse.add?.(passages);
|
|
131
|
+
else if (options.load)
|
|
132
|
+
await options.load(passages);
|
|
133
|
+
else
|
|
134
|
+
return 'a retriever without add() needs a load() to fill what it reads from';
|
|
135
|
+
await settle();
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
await suite.check('finds the passage holding the query terms first', async () => {
|
|
139
|
+
const [first] = await retriever.retrieve('warehouse business days', { topK: 3 });
|
|
140
|
+
return first?.id === 'shipping#0' || `expected shipping#0, got ${first?.id}`;
|
|
141
|
+
});
|
|
142
|
+
await suite.check('returns at most topK results, best first', async () => {
|
|
143
|
+
const results = await retriever.retrieve('refund refunds return', { topK: 1 });
|
|
144
|
+
return results.length <= 1 || `${results.length} results for topK 1`;
|
|
145
|
+
});
|
|
146
|
+
if (options.capabilities?.filters === false)
|
|
147
|
+
suite.skip('narrows by metadata', 'the retriever declares no filters');
|
|
148
|
+
else {
|
|
149
|
+
await suite.check('narrows by metadata', async () => {
|
|
150
|
+
const results = await retriever.retrieve('refund', { filter: { tenant: 'globex' }, topK: 10 });
|
|
151
|
+
return ((results.every((result) => result.metadata?.tenant === 'globex') && results.length > 0) ||
|
|
152
|
+
`got ${results.map((result) => result.id).join(', ')}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (!writes) {
|
|
156
|
+
suite.skip('replaces a passage added again under its id', 'the retriever does not take writes');
|
|
157
|
+
suite.skip('deletes by id, ignoring ids it does not hold', 'the retriever does not take writes');
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
await suite.check('replaces a passage added again under its id', async () => {
|
|
161
|
+
await sparse.add?.([{ id: 'careers#0', content: 'Engineers relocate with a stipend' }]);
|
|
162
|
+
await settle();
|
|
163
|
+
const results = await retriever.retrieve('stipend relocate', { topK: 10 });
|
|
164
|
+
return (results.filter((result) => result.id === 'careers#0').length === 1 || 'the passage is missing or duplicated');
|
|
165
|
+
});
|
|
166
|
+
await suite.check('deletes by id, ignoring ids it does not hold', async () => {
|
|
167
|
+
await sparse.delete?.(['shipping#0', 'not-stored']);
|
|
168
|
+
await settle();
|
|
169
|
+
const results = await retriever.retrieve('warehouse business days', { topK: 10 });
|
|
170
|
+
return !results.some((result) => result.id === 'shipping#0') || 'the deleted passage is still found';
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
await suite.check('answers an empty query without failing', async () => {
|
|
174
|
+
await retriever.retrieve('', { topK: 3 });
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
return suite.checks;
|
|
178
|
+
}
|
|
179
|
+
export async function runStoreContract(store, options = {}) {
|
|
180
|
+
const suite = new Suite();
|
|
181
|
+
const capabilities = options.capabilities ?? {};
|
|
182
|
+
const namespace = ['contract', 'users'];
|
|
183
|
+
await suite.check('round-trips a value', async () => {
|
|
184
|
+
await store.put(namespace, 'ann', { name: 'Ann', plan: 'pro' });
|
|
185
|
+
const item = await store.get(namespace, 'ann');
|
|
186
|
+
return (item?.value.name === 'Ann' && item.key === 'ann') || `read ${JSON.stringify(item)}`;
|
|
187
|
+
});
|
|
188
|
+
await suite.check('keeps the creation time when an item is replaced', async () => {
|
|
189
|
+
const before = await store.get(namespace, 'ann');
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
191
|
+
await store.put(namespace, 'ann', { name: 'Ann', plan: 'team' });
|
|
192
|
+
const after = await store.get(namespace, 'ann');
|
|
193
|
+
if (after?.value.plan !== 'team')
|
|
194
|
+
return 'the replacement was not stored';
|
|
195
|
+
return after.createdAt === before?.createdAt || `createdAt moved from ${before?.createdAt} to ${after.createdAt}`;
|
|
196
|
+
});
|
|
197
|
+
await suite.check('reads a missing key as nothing', async () => (await store.get(namespace, 'nobody')) === undefined);
|
|
198
|
+
await suite.check('deletes an item', async () => {
|
|
199
|
+
await store.put(namespace, 'temp', { name: 'Temp' });
|
|
200
|
+
await store.delete(namespace, 'temp');
|
|
201
|
+
return (await store.get(namespace, 'temp')) === undefined;
|
|
202
|
+
});
|
|
203
|
+
if (capabilities.search === false)
|
|
204
|
+
suite.skip('lists a namespace prefix, with paging', 'the store declares no search');
|
|
205
|
+
else {
|
|
206
|
+
await suite.check('lists a namespace prefix, with paging', async () => {
|
|
207
|
+
await store.put([...namespace, 'archived'], 'bob', { name: 'Bob', plan: 'free' });
|
|
208
|
+
await store.put(namespace, 'cy', { name: 'Cy', plan: 'free' });
|
|
209
|
+
const all = await store.search(['contract'], { limit: 10 });
|
|
210
|
+
if (all.length !== 3)
|
|
211
|
+
return `expected 3 items under the prefix, got ${all.length}`;
|
|
212
|
+
const page = await store.search(['contract'], { limit: 2, offset: 2 });
|
|
213
|
+
return page.length === 1 || `expected 1 item on the second page, got ${page.length}`;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (capabilities.search === false || capabilities.filters === false)
|
|
217
|
+
suite.skip('filters by a value field', 'the store declares no filters');
|
|
218
|
+
else {
|
|
219
|
+
await suite.check('filters by a value field', async () => {
|
|
220
|
+
const free = await store.search(['contract'], { filter: { plan: 'free' }, limit: 10 });
|
|
221
|
+
const names = free
|
|
222
|
+
.map((item) => item.value.name)
|
|
223
|
+
.sort()
|
|
224
|
+
.join(',');
|
|
225
|
+
return names === 'Bob,Cy' || `got ${names}`;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (capabilities.namespaces === false)
|
|
229
|
+
suite.skip('lists namespaces under a prefix', 'the store declares no namespace listing');
|
|
230
|
+
else {
|
|
231
|
+
await suite.check('lists namespaces under a prefix', async () => {
|
|
232
|
+
const listed = (await store.listNamespaces({ prefix: ['contract'] })).map((parts) => parts.join('/')).sort();
|
|
233
|
+
return listed.join(',') === 'contract/users,contract/users/archived' || `got ${listed.join(', ')}`;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return suite.checks;
|
|
237
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One vocabulary for adapter failures, so code above an adapter retries, alerts, and reports the same
|
|
3
|
+
* way whichever database, provider, or service the adapter talks to.
|
|
4
|
+
*/
|
|
5
|
+
/** What kind of failure an adapter call met. */
|
|
6
|
+
export type AdapterErrorCode = 'AUTH' | 'RATE_LIMIT' | 'NOT_FOUND' | 'INVALID' | 'CONFLICT' | 'TIMEOUT' | 'UNAVAILABLE' | 'CANCELLED' | 'UNKNOWN';
|
|
7
|
+
/** A failure's code, and whether another attempt could succeed. */
|
|
8
|
+
export interface AdapterErrorClass {
|
|
9
|
+
/** The kind of failure. */
|
|
10
|
+
code: AdapterErrorCode;
|
|
11
|
+
/** True for a failure a retry could get past: a rate limit, a timeout, an unavailable service. */
|
|
12
|
+
retryable: boolean;
|
|
13
|
+
/** The HTTP status, when the failure carried one. */
|
|
14
|
+
status?: number;
|
|
15
|
+
}
|
|
16
|
+
/** A failed adapter call, normalized. The original error is its `cause`. */
|
|
17
|
+
export declare class AdapterError extends Error {
|
|
18
|
+
/** The adapter that failed. */
|
|
19
|
+
readonly adapter: string;
|
|
20
|
+
/** The method that failed, such as `search`. */
|
|
21
|
+
readonly operation: string;
|
|
22
|
+
/** The kind of failure. */
|
|
23
|
+
readonly code: AdapterErrorCode;
|
|
24
|
+
/** Whether another attempt could succeed. */
|
|
25
|
+
readonly retryable: boolean;
|
|
26
|
+
/** The HTTP status, when the failure carried one. */
|
|
27
|
+
readonly status?: number;
|
|
28
|
+
constructor(
|
|
29
|
+
/** The adapter that failed. */
|
|
30
|
+
adapter: string,
|
|
31
|
+
/** The method that failed, such as `search`. */
|
|
32
|
+
operation: string, failure: AdapterErrorClass, cause: unknown);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Classifies any error by what it carries: an HTTP status (`status`, `statusCode`, or
|
|
36
|
+
* `response.status`), a Node network code such as `ECONNREFUSED`, or an `AbortError` or
|
|
37
|
+
* `TimeoutError` name. An adapter's own `normalizeError` runs first and may override it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function classifyAdapterError(error: unknown): AdapterErrorClass;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class AdapterError extends Error {
|
|
2
|
+
adapter;
|
|
3
|
+
operation;
|
|
4
|
+
code;
|
|
5
|
+
retryable;
|
|
6
|
+
status;
|
|
7
|
+
constructor(adapter, operation, failure, cause) {
|
|
8
|
+
super(`${adapter} ${operation} failed (${failure.code}): ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
9
|
+
cause,
|
|
10
|
+
});
|
|
11
|
+
this.adapter = adapter;
|
|
12
|
+
this.operation = operation;
|
|
13
|
+
this.name = 'AdapterError';
|
|
14
|
+
this.code = failure.code;
|
|
15
|
+
this.retryable = failure.retryable;
|
|
16
|
+
if (failure.status !== undefined)
|
|
17
|
+
this.status = failure.status;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const RETRYABLE = new Set(['RATE_LIMIT', 'TIMEOUT', 'UNAVAILABLE']);
|
|
21
|
+
export function classifyAdapterError(error) {
|
|
22
|
+
const value = error;
|
|
23
|
+
const raw = value?.status ?? value?.statusCode ?? value?.response?.status;
|
|
24
|
+
const status = typeof raw === 'number' ? raw : undefined;
|
|
25
|
+
const code = (() => {
|
|
26
|
+
if (value?.name === 'AbortError')
|
|
27
|
+
return 'CANCELLED';
|
|
28
|
+
if (value?.name === 'TimeoutError' || value?.code === 'ETIMEDOUT')
|
|
29
|
+
return 'TIMEOUT';
|
|
30
|
+
if (status !== undefined) {
|
|
31
|
+
if (status === 401 || status === 403)
|
|
32
|
+
return 'AUTH';
|
|
33
|
+
if (status === 404)
|
|
34
|
+
return 'NOT_FOUND';
|
|
35
|
+
if (status === 408)
|
|
36
|
+
return 'TIMEOUT';
|
|
37
|
+
if (status === 409 || status === 412)
|
|
38
|
+
return 'CONFLICT';
|
|
39
|
+
if (status === 429)
|
|
40
|
+
return 'RATE_LIMIT';
|
|
41
|
+
if (status >= 500)
|
|
42
|
+
return 'UNAVAILABLE';
|
|
43
|
+
if (status >= 400)
|
|
44
|
+
return 'INVALID';
|
|
45
|
+
}
|
|
46
|
+
if (typeof value?.code === 'string' && /^(ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EPIPE)$/.test(value.code)) {
|
|
47
|
+
return 'UNAVAILABLE';
|
|
48
|
+
}
|
|
49
|
+
return 'UNKNOWN';
|
|
50
|
+
})();
|
|
51
|
+
return { code, retryable: RETRYABLE.has(code), ...(status === undefined ? {} : { status }) };
|
|
52
|
+
}
|