nexus-ai-pro 2.3.0 → 2.5.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 +271 -0
- package/CHANGELOG.md +330 -1
- package/README.md +18 -4
- package/SECURITY.md +209 -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/middleware/pii.js +2 -33
- package/dist/context-hub/hub.d.ts +53 -0
- package/dist/context-hub/hub.js +108 -3
- package/dist/context-hub/index.d.ts +2 -1
- package/dist/context-hub/index.js +2 -1
- package/dist/context-hub/signing.d.ts +95 -0
- package/dist/context-hub/signing.js +88 -0
- package/dist/core/secure-stream.js +4 -3
- package/dist/doctor/index.d.ts +4 -2
- package/dist/doctor/index.js +29 -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/threads.d.ts +187 -0
- package/dist/evaluate/threads.js +187 -0
- package/dist/evaluate/version.js +3 -3
- package/dist/graph/graph.js +2 -2
- 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/operations/adapters.d.ts +2 -1
- package/dist/operations/runner.js +2 -0
- package/dist/operations/stats.js +11 -4
- package/dist/postgres/index.d.ts +2 -1
- package/dist/postgres/index.js +4 -0
- package/dist/postgres/operations.js +11 -4
- package/dist/postgres/store.d.ts +7 -0
- package/dist/postgres/store.js +21 -0
- package/dist/postgres/tenancy.d.ts +43 -0
- package/dist/postgres/tenancy.js +71 -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/file-ingestion.js +15 -9
- 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/deployments.d.ts +188 -11
- package/dist/server/deployments.js +474 -36
- package/dist/server/metrics.d.ts +2 -0
- package/dist/server/metrics.js +7 -0
- package/dist/server/runs.d.ts +18 -1
- package/dist/server/runs.js +66 -7
- package/dist/server/server.js +9 -1
- package/dist/server/state.d.ts +12 -2
- package/dist/server/state.js +15 -0
- package/dist/sqlite/operations.js +15 -4
- package/dist/sqlite/store.d.ts +7 -0
- package/dist/sqlite/store.js +28 -0
- package/dist/store/memory.d.ts +8 -0
- package/dist/store/memory.js +12 -1
- package/dist/store/redis.d.ts +13 -0
- package/dist/store/redis.js +38 -0
- package/dist/testing/record.js +2 -2
- package/dist/tracing/instrument.js +1 -0
- package/dist/tracing/stores.js +2 -1
- package/dist/tracing/tracer.d.ts +4 -0
- package/dist/tracing/tracer.js +8 -2
- package/dist/types/config.d.ts +20 -0
- package/dist/types/operations.d.ts +7 -0
- package/dist/types/server.d.ts +67 -2
- package/dist/types/store.d.ts +9 -0
- 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/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/middleware/pii.js +4 -35
- package/dist-cjs/context-hub/hub.d.ts +53 -0
- package/dist-cjs/context-hub/hub.js +110 -4
- package/dist-cjs/context-hub/index.d.ts +2 -1
- package/dist-cjs/context-hub/index.js +7 -1
- package/dist-cjs/context-hub/signing.d.ts +95 -0
- package/dist-cjs/context-hub/signing.js +96 -0
- package/dist-cjs/core/secure-stream.js +4 -3
- package/dist-cjs/doctor/index.d.ts +4 -2
- package/dist-cjs/doctor/index.js +29 -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/threads.d.ts +187 -0
- package/dist-cjs/evaluate/threads.js +198 -0
- package/dist-cjs/evaluate/version.js +3 -3
- package/dist-cjs/graph/graph.js +2 -2
- 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/operations/adapters.d.ts +2 -1
- package/dist-cjs/operations/runner.js +2 -0
- package/dist-cjs/operations/stats.js +11 -4
- package/dist-cjs/postgres/index.d.ts +2 -1
- package/dist-cjs/postgres/index.js +8 -1
- package/dist-cjs/postgres/operations.js +11 -4
- package/dist-cjs/postgres/store.d.ts +7 -0
- package/dist-cjs/postgres/store.js +21 -0
- package/dist-cjs/postgres/tenancy.d.ts +43 -0
- package/dist-cjs/postgres/tenancy.js +110 -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/file-ingestion.js +14 -8
- 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/deployments.d.ts +188 -11
- package/dist-cjs/server/deployments.js +474 -36
- package/dist-cjs/server/metrics.d.ts +2 -0
- package/dist-cjs/server/metrics.js +8 -0
- package/dist-cjs/server/runs.d.ts +18 -1
- package/dist-cjs/server/runs.js +65 -6
- package/dist-cjs/server/server.js +9 -1
- package/dist-cjs/server/state.d.ts +12 -2
- package/dist-cjs/server/state.js +15 -0
- package/dist-cjs/sqlite/operations.js +15 -4
- package/dist-cjs/sqlite/store.d.ts +7 -0
- package/dist-cjs/sqlite/store.js +28 -0
- package/dist-cjs/store/memory.d.ts +8 -0
- package/dist-cjs/store/memory.js +12 -1
- package/dist-cjs/store/redis.d.ts +13 -0
- package/dist-cjs/store/redis.js +38 -0
- package/dist-cjs/testing/record.js +2 -2
- package/dist-cjs/tracing/instrument.js +1 -0
- package/dist-cjs/tracing/stores.js +2 -1
- package/dist-cjs/tracing/tracer.d.ts +4 -0
- package/dist-cjs/tracing/tracer.js +8 -2
- package/dist-cjs/types/config.d.ts +20 -0
- package/dist-cjs/types/operations.d.ts +7 -0
- package/dist-cjs/types/server.d.ts +67 -2
- package/dist-cjs/types/store.d.ts +9 -0
- 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/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +5 -2
- package/package.json +102 -6
package/dist/mcp/client.js
CHANGED
|
@@ -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:
|
|
202
|
+
env: serverEnvironment(process.env, options),
|
|
145
203
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
146
204
|
});
|
|
147
205
|
spawned.stdout.setEncoding('utf8');
|
package/dist/mcp/registry.d.ts
CHANGED
|
@@ -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. */
|
package/dist/mcp/registry.js
CHANGED
|
@@ -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 &&
|
|
@@ -58,7 +58,8 @@ export declare class RedisOperationStore<TResult = unknown> implements Operation
|
|
|
58
58
|
private readonly client;
|
|
59
59
|
private readonly prefix;
|
|
60
60
|
private readonly useEval;
|
|
61
|
-
|
|
61
|
+
/** Whether queued and leased work is kept in sorted sets, as `index: true` asks. */
|
|
62
|
+
readonly indexed: boolean;
|
|
62
63
|
constructor(client: RedisOperationLikeClient, options?: RedisOperationStoreOptions);
|
|
63
64
|
/**
|
|
64
65
|
* Stores a new record and indexes its idempotency key. Throws `OperationDuplicateError` when another
|
|
@@ -51,6 +51,8 @@ export class OperationRunner {
|
|
|
51
51
|
for (const record of candidates) {
|
|
52
52
|
if (claimed.length >= limit)
|
|
53
53
|
break;
|
|
54
|
+
if (!matchesFilter(record, filter))
|
|
55
|
+
continue;
|
|
54
56
|
if (record.expiresAt && record.expiresAt <= nowIso) {
|
|
55
57
|
await this.settleExpired(record);
|
|
56
58
|
continue;
|
package/dist/operations/stats.js
CHANGED
|
@@ -5,29 +5,36 @@ export function isUnheldQueued(record, now) {
|
|
|
5
5
|
export function matchesFilter(record, filter) {
|
|
6
6
|
if (filter?.tenantId !== undefined && record.tenantId !== filter.tenantId)
|
|
7
7
|
return false;
|
|
8
|
+
if (filter?.kinds && !filter.kinds.includes(record.kind ?? ''))
|
|
9
|
+
return false;
|
|
8
10
|
return !filter?.kindPrefix || (record.kind ?? '').startsWith(filter.kindPrefix);
|
|
9
11
|
}
|
|
10
12
|
export function countRecords(records, now, filter) {
|
|
11
13
|
const byStatus = {};
|
|
12
14
|
let oldestQueuedAt;
|
|
13
15
|
let lapsedLeases = 0;
|
|
16
|
+
const queuedByKind = {};
|
|
14
17
|
for (const record of records) {
|
|
15
18
|
if (isTerminalOperationStatus(record.status) || !matchesFilter(record, filter))
|
|
16
19
|
continue;
|
|
17
20
|
byStatus[record.status] = (byStatus[record.status] ?? 0) + 1;
|
|
18
|
-
if (record.status === 'queued'
|
|
19
|
-
oldestQueuedAt
|
|
21
|
+
if (record.status === 'queued') {
|
|
22
|
+
if (!oldestQueuedAt || record.createdAt < oldestQueuedAt)
|
|
23
|
+
oldestQueuedAt = record.createdAt;
|
|
24
|
+
const kind = record.kind ?? '';
|
|
25
|
+
queuedByKind[kind] = (queuedByKind[kind] ?? 0) + 1;
|
|
20
26
|
}
|
|
21
27
|
if (record.status === 'running' && (!record.lease || record.lease.expiresAt <= now))
|
|
22
28
|
lapsedLeases += 1;
|
|
23
29
|
}
|
|
24
|
-
return { byStatus, ...(oldestQueuedAt ? { oldestQueuedAt } : {}), lapsedLeases };
|
|
30
|
+
return { byStatus, ...(oldestQueuedAt ? { oldestQueuedAt } : {}), lapsedLeases, queuedByKind };
|
|
25
31
|
}
|
|
26
32
|
export async function operationStats(store, options = {}) {
|
|
27
33
|
const now = (options.now ?? new Date()).toISOString();
|
|
28
|
-
const filter = options.kindPrefix || options.tenantId !== undefined
|
|
34
|
+
const filter = options.kindPrefix || options.kinds || options.tenantId !== undefined
|
|
29
35
|
? {
|
|
30
36
|
...(options.kindPrefix ? { kindPrefix: options.kindPrefix } : {}),
|
|
37
|
+
...(options.kinds ? { kinds: options.kinds } : {}),
|
|
31
38
|
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
32
39
|
}
|
|
33
40
|
: undefined;
|
package/dist/postgres/index.d.ts
CHANGED
|
@@ -9,9 +9,10 @@ export { PostgresPromptStore, type PostgresPromptStoreOptions, promptStoreMigrat
|
|
|
9
9
|
export { PostgresRollupStore, type PostgresRollupStoreOptions, rollupStoreMigration, rollupStoreMigrations, } from './rollups.js';
|
|
10
10
|
export { PostgresStore, type PostgresStoreOptions, storeMigration, storeMigrations } from './store.js';
|
|
11
11
|
export { PostgresTraceStore, type PostgresTraceStoreOptions, traceStoreMigration, traceStoreMigrations, } from './traces.js';
|
|
12
|
+
export { PostgresTenantUsage, type PostgresTenantUsageOptions, tenantUsageMigration, tenantUsageMigrations, } from './tenancy.js';
|
|
12
13
|
export { PostgresVectorStore, type PostgresVectorStoreOptions, vectorStoreMigration, vectorStoreMigrations, } from './vectors.js';
|
|
13
14
|
/** The Postgres adapters a migration can include. */
|
|
14
|
-
export type PostgresAdapter = 'operations' | 'store' | 'traces' | 'evaluation' | 'circuits' | 'prompts' | 'rollups' | 'vectors';
|
|
15
|
+
export type PostgresAdapter = 'operations' | 'store' | 'traces' | 'evaluation' | 'circuits' | 'tenancy' | 'prompts' | 'rollups' | 'vectors';
|
|
15
16
|
/** Options for `postgresMigrations()` and `postgresMigration()`. */
|
|
16
17
|
export interface PostgresMigrationOptions {
|
|
17
18
|
/**
|
package/dist/postgres/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { operationStoreMigrations } from './operations.js';
|
|
|
7
7
|
import { promptStoreMigrations } from './prompts.js';
|
|
8
8
|
import { rollupStoreMigrations } from './rollups.js';
|
|
9
9
|
import { storeMigrations } from './store.js';
|
|
10
|
+
import { tenantUsageMigrations } from './tenancy.js';
|
|
10
11
|
import { traceStoreMigrations } from './traces.js';
|
|
11
12
|
import { vectorStoreMigrations } from './vectors.js';
|
|
12
13
|
export { migrationChecksum, SchemaMigrationError, } from '../utils/schema-migrations.js';
|
|
@@ -19,6 +20,7 @@ export { PostgresPromptStore, promptStoreMigration, promptStoreMigrations, } fro
|
|
|
19
20
|
export { PostgresRollupStore, rollupStoreMigration, rollupStoreMigrations, } from './rollups.js';
|
|
20
21
|
export { PostgresStore, storeMigration, storeMigrations } from './store.js';
|
|
21
22
|
export { PostgresTraceStore, traceStoreMigration, traceStoreMigrations, } from './traces.js';
|
|
23
|
+
export { PostgresTenantUsage, tenantUsageMigration, tenantUsageMigrations, } from './tenancy.js';
|
|
22
24
|
export { PostgresVectorStore, vectorStoreMigration, vectorStoreMigrations, } from './vectors.js';
|
|
23
25
|
export const POSTGRES_ADAPTERS = [
|
|
24
26
|
'operations',
|
|
@@ -26,6 +28,7 @@ export const POSTGRES_ADAPTERS = [
|
|
|
26
28
|
'traces',
|
|
27
29
|
'evaluation',
|
|
28
30
|
'circuits',
|
|
31
|
+
'tenancy',
|
|
29
32
|
'prompts',
|
|
30
33
|
'rollups',
|
|
31
34
|
'vectors',
|
|
@@ -39,6 +42,7 @@ export function postgresMigrations(options = {}) {
|
|
|
39
42
|
traces: () => traceStoreMigrations(),
|
|
40
43
|
evaluation: () => evaluationStoreMigrations(),
|
|
41
44
|
circuits: () => circuitStoreMigrations(),
|
|
45
|
+
tenancy: () => tenantUsageMigrations(),
|
|
42
46
|
prompts: () => promptStoreMigrations(),
|
|
43
47
|
rollups: () => rollupStoreMigrations(),
|
|
44
48
|
vectors: () => {
|
|
@@ -131,12 +131,12 @@ export class PostgresOperationStore {
|
|
|
131
131
|
async stats(now, filter) {
|
|
132
132
|
const params = [now];
|
|
133
133
|
const narrowed = filterSql(filter, params);
|
|
134
|
-
const { rows } = await this.client.query(`SELECT status, count(*)::int AS count,
|
|
134
|
+
const { rows } = await this.client.query(`SELECT status, kind, count(*)::int AS count,
|
|
135
135
|
min(created_at) FILTER (WHERE status = 'queued') AS oldest,
|
|
136
136
|
count(*) FILTER (WHERE status = 'running' AND (lease_expires_at IS NULL OR lease_expires_at <= $1))::int AS lapsed
|
|
137
137
|
FROM ${this.table}
|
|
138
138
|
WHERE status NOT IN (${TERMINAL})${narrowed}
|
|
139
|
-
GROUP BY status`, params);
|
|
139
|
+
GROUP BY status, kind`, params);
|
|
140
140
|
return statsFromRows(rows);
|
|
141
141
|
}
|
|
142
142
|
async prune(before) {
|
|
@@ -163,6 +163,10 @@ function filterSql(filter, params) {
|
|
|
163
163
|
params.push(filter.kindPrefix);
|
|
164
164
|
sql += ` AND substr(kind, 1, char_length($${params.length})) = $${params.length}`;
|
|
165
165
|
}
|
|
166
|
+
if (filter?.kinds) {
|
|
167
|
+
params.push([...filter.kinds]);
|
|
168
|
+
sql += ` AND kind = ANY($${params.length}::text[])`;
|
|
169
|
+
}
|
|
166
170
|
if (filter?.tenantId !== undefined) {
|
|
167
171
|
params.push(filter.tenantId);
|
|
168
172
|
sql += ` AND doc ->> 'tenantId' = $${params.length}`;
|
|
@@ -173,11 +177,14 @@ function statsFromRows(rows) {
|
|
|
173
177
|
const byStatus = {};
|
|
174
178
|
let oldestQueuedAt;
|
|
175
179
|
let lapsedLeases = 0;
|
|
180
|
+
const queuedByKind = {};
|
|
176
181
|
for (const row of rows) {
|
|
177
|
-
byStatus[row.status] = Number(row.count);
|
|
182
|
+
byStatus[row.status] = (byStatus[row.status] ?? 0) + Number(row.count);
|
|
183
|
+
if (row.status === 'queued')
|
|
184
|
+
queuedByKind[row.kind ?? ''] = (queuedByKind[row.kind ?? ''] ?? 0) + Number(row.count);
|
|
178
185
|
if (row.oldest && (!oldestQueuedAt || row.oldest < oldestQueuedAt))
|
|
179
186
|
oldestQueuedAt = row.oldest;
|
|
180
187
|
lapsedLeases += Number(row.lapsed);
|
|
181
188
|
}
|
|
182
|
-
return { byStatus, ...(oldestQueuedAt ? { oldestQueuedAt } : {}), lapsedLeases };
|
|
189
|
+
return { byStatus, ...(oldestQueuedAt ? { oldestQueuedAt } : {}), lapsedLeases, queuedByKind };
|
|
183
190
|
}
|
package/dist/postgres/store.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export declare class PostgresStore implements Store {
|
|
|
44
44
|
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
45
45
|
/** Stores an item, keeping its original `createdAt` when it replaces one. */
|
|
46
46
|
put<V>(namespace: StoreNamespace, key: string, value: V, options?: StorePutOptions): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Stores an item only when the stored one's `version` is `expected`, or, with `expected` `null`,
|
|
49
|
+
* only when none is stored. One statement, so two writers deciding on one version cannot both win.
|
|
50
|
+
*/
|
|
51
|
+
putIfVersion<V extends {
|
|
52
|
+
version: number;
|
|
53
|
+
}>(namespace: StoreNamespace, key: string, value: V, expected: number | null): Promise<boolean>;
|
|
47
54
|
/** Reads an item, or `undefined` when it does not exist or has expired. */
|
|
48
55
|
get<V>(namespace: StoreNamespace, key: string): Promise<StoreItem<V> | undefined>;
|
|
49
56
|
/** Deletes an item. */
|
package/dist/postgres/store.js
CHANGED
|
@@ -71,6 +71,27 @@ export class PostgresStore {
|
|
|
71
71
|
vector ? JSON.stringify(vector) : null,
|
|
72
72
|
]);
|
|
73
73
|
}
|
|
74
|
+
async putIfVersion(namespace, key, value, expected) {
|
|
75
|
+
if (!key.trim())
|
|
76
|
+
throw new RangeError('A store key must not be empty');
|
|
77
|
+
const timestamp = this.now().toISOString();
|
|
78
|
+
const vector = await this.embed(value, undefined);
|
|
79
|
+
const embedding = vector ? JSON.stringify(vector) : null;
|
|
80
|
+
const cast = this.options.vectorDimensions ? '::vector' : '::jsonb';
|
|
81
|
+
const { rows } = expected === null
|
|
82
|
+
?
|
|
83
|
+
await this.client.query(`INSERT INTO ${this.table} (ns_key, key, namespace, value, created_at, updated_at, expires_at, embedding)
|
|
84
|
+
VALUES ($1, $2, $3::jsonb, $4::jsonb, $5, $5, NULL, $6${cast})
|
|
85
|
+
ON CONFLICT (ns_key, key) DO UPDATE SET value = EXCLUDED.value, created_at = EXCLUDED.created_at,
|
|
86
|
+
updated_at = EXCLUDED.updated_at, expires_at = NULL, embedding = EXCLUDED.embedding
|
|
87
|
+
WHERE ${this.table}.expires_at IS NOT NULL AND ${this.table}.expires_at <= $5
|
|
88
|
+
RETURNING key`, [namespaceKey(namespace), key, JSON.stringify([...namespace]), JSON.stringify(value), timestamp, embedding])
|
|
89
|
+
: await this.client.query(`UPDATE ${this.table} SET value = $3::jsonb, updated_at = $4, expires_at = NULL, embedding = $5${cast}
|
|
90
|
+
WHERE ns_key = $1 AND key = $2 AND (expires_at IS NULL OR expires_at > $4)
|
|
91
|
+
AND jsonb_typeof(value -> 'version') = 'number' AND (value ->> 'version')::numeric = $6
|
|
92
|
+
RETURNING key`, [namespaceKey(namespace), key, JSON.stringify(value), timestamp, embedding, expected]);
|
|
93
|
+
return rows.length > 0;
|
|
94
|
+
}
|
|
74
95
|
async get(namespace, key) {
|
|
75
96
|
const { rows } = await this.client.query(`SELECT namespace::text AS namespace, key, value::text AS value, created_at, updated_at, expires_at FROM ${this.table}
|
|
76
97
|
WHERE ns_key = $1 AND key = $2 AND (expires_at IS NULL OR expires_at > $3)`, [namespaceKey(namespace), key, this.now().toISOString()]);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TenantUsageStore } from '../server/tenancy.js';
|
|
2
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
3
|
+
import { type PostgresLikeClient } from './client.js';
|
|
4
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
5
|
+
/** Options for `PostgresTenantUsage`. */
|
|
6
|
+
export interface PostgresTenantUsageOptions {
|
|
7
|
+
/** Table name, optionally schema-qualified. Defaults to `nexus_tenant_usage`. */
|
|
8
|
+
table?: string;
|
|
9
|
+
/** Clock for slot leases and spending periods, in epoch milliseconds. Defaults to `Date.now`. */
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
/** The schema, as statements: every migration's, in order. */
|
|
13
|
+
export declare function tenantUsageMigration(options?: Pick<PostgresTenantUsageOptions, 'table'>): string[];
|
|
14
|
+
/** The versioned schema, which `migrate()` and `nexus db migrate` apply. */
|
|
15
|
+
export declare function tenantUsageMigrations(options?: Pick<PostgresTenantUsageOptions, 'table'>): SchemaMigration[];
|
|
16
|
+
/**
|
|
17
|
+
* Tenant usage in Postgres, shared by every replica, for a deployment that already runs Postgres
|
|
18
|
+
* and not Redis. Pass it to `tenantLimits({ usage })`.
|
|
19
|
+
*
|
|
20
|
+
* Each key is one row: a tenant's active slots as a JSON map from run to lease expiry, or its spending
|
|
21
|
+
* in the current period. Every step is a single statement on that row, and Postgres applies
|
|
22
|
+
* concurrent updates of one row one at a time, re-checking each against the newest version. Two
|
|
23
|
+
* replicas admitting runs for one tenant at the same moment therefore cannot both take its last slot.
|
|
24
|
+
*/
|
|
25
|
+
export declare class PostgresTenantUsage implements TenantUsageStore {
|
|
26
|
+
private readonly client;
|
|
27
|
+
private readonly options;
|
|
28
|
+
private readonly table;
|
|
29
|
+
private readonly now;
|
|
30
|
+
constructor(client: PostgresLikeClient, options?: PostgresTenantUsageOptions);
|
|
31
|
+
/** Creates the table, or brings it up to date. */
|
|
32
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
33
|
+
/** Takes a slot for `member` unless `max` live slots are held. Holding one already counts as taken. */
|
|
34
|
+
acquire(key: string, member: string, max: number, ttlMs: number): Promise<boolean>;
|
|
35
|
+
/** Frees a slot. Freeing one that is not held does nothing. */
|
|
36
|
+
release(key: string, member: string): Promise<void>;
|
|
37
|
+
/** Live slots held under a key. */
|
|
38
|
+
count(key: string): Promise<number>;
|
|
39
|
+
/** Adds to a total that resets at `resetAt`, and resolves to the new total. */
|
|
40
|
+
add(key: string, amount: number, resetAt: number): Promise<number>;
|
|
41
|
+
/** A total, or 0 once its period has reset. */
|
|
42
|
+
total(key: string): Promise<number>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { quoteTable } from './client.js';
|
|
2
|
+
export function tenantUsageMigration(options = {}) {
|
|
3
|
+
return tenantUsageMigrations(options).flatMap((migration) => [...migration.statements]);
|
|
4
|
+
}
|
|
5
|
+
export function tenantUsageMigrations(options = {}) {
|
|
6
|
+
const name = options.table ?? 'nexus_tenant_usage';
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
component: `tenancy:${name}`,
|
|
10
|
+
version: 1,
|
|
11
|
+
name: 'create the tenant usage table',
|
|
12
|
+
statements: [
|
|
13
|
+
`CREATE TABLE IF NOT EXISTS ${quoteTable(name)} (
|
|
14
|
+
key text PRIMARY KEY,
|
|
15
|
+
slots jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
16
|
+
total double precision NOT NULL DEFAULT 0,
|
|
17
|
+
reset_at double precision
|
|
18
|
+
)`,
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
export class PostgresTenantUsage {
|
|
24
|
+
client;
|
|
25
|
+
options;
|
|
26
|
+
table;
|
|
27
|
+
now;
|
|
28
|
+
constructor(client, options = {}) {
|
|
29
|
+
this.client = client;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.table = quoteTable(options.table ?? 'nexus_tenant_usage');
|
|
32
|
+
this.now = options.now ?? Date.now;
|
|
33
|
+
}
|
|
34
|
+
async migrate(options = {}) {
|
|
35
|
+
const { applyPostgresMigrations } = await import('./migrations.js');
|
|
36
|
+
return applyPostgresMigrations(this.client, tenantUsageMigrations(this.options), options);
|
|
37
|
+
}
|
|
38
|
+
async acquire(key, member, max, ttlMs) {
|
|
39
|
+
const now = this.now();
|
|
40
|
+
await this.client.query(`INSERT INTO ${this.table} (key) VALUES ($1) ON CONFLICT (key) DO NOTHING`, [key]);
|
|
41
|
+
const live = `(SELECT COALESCE(jsonb_object_agg(slot.member, slot.expires), '{}'::jsonb)
|
|
42
|
+
FROM jsonb_each(${this.table}.slots) AS slot(member, expires)
|
|
43
|
+
WHERE (slot.expires #>> '{}')::double precision > $3)`;
|
|
44
|
+
const { rows } = await this.client.query(`UPDATE ${this.table}
|
|
45
|
+
SET slots = CASE WHEN ${live} ? $2 THEN ${live} ELSE ${live} || jsonb_build_object($2::text, $4::double precision) END
|
|
46
|
+
WHERE key = $1 AND (${live} ? $2 OR (SELECT count(*) FROM jsonb_object_keys(${live})) < $5)
|
|
47
|
+
RETURNING key`, [key, member, now, now + ttlMs, max]);
|
|
48
|
+
return rows.length > 0;
|
|
49
|
+
}
|
|
50
|
+
async release(key, member) {
|
|
51
|
+
await this.client.query(`UPDATE ${this.table} SET slots = slots - $2::text WHERE key = $1`, [key, member]);
|
|
52
|
+
}
|
|
53
|
+
async count(key) {
|
|
54
|
+
const { rows } = await this.client.query(`SELECT count(*)::int AS held FROM ${this.table}, jsonb_each(${this.table}.slots) AS slot(member, expires)
|
|
55
|
+
WHERE key = $1 AND (slot.expires #>> '{}')::double precision > $2`, [key, this.now()]);
|
|
56
|
+
return Number(rows[0]?.held ?? 0);
|
|
57
|
+
}
|
|
58
|
+
async add(key, amount, resetAt) {
|
|
59
|
+
const { rows } = await this.client.query(`INSERT INTO ${this.table} (key, total, reset_at) VALUES ($1, $2, $3)
|
|
60
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
61
|
+
total = CASE WHEN ${this.table}.reset_at IS NOT NULL AND ${this.table}.reset_at <= $4
|
|
62
|
+
THEN EXCLUDED.total ELSE ${this.table}.total + EXCLUDED.total END,
|
|
63
|
+
reset_at = EXCLUDED.reset_at
|
|
64
|
+
RETURNING total`, [key, amount, resetAt, this.now()]);
|
|
65
|
+
return Number(rows[0]?.total ?? 0);
|
|
66
|
+
}
|
|
67
|
+
async total(key) {
|
|
68
|
+
const { rows } = await this.client.query(`SELECT CASE WHEN reset_at IS NULL OR reset_at > $2 THEN total ELSE 0 END AS total FROM ${this.table} WHERE key = $1`, [key, this.now()]);
|
|
69
|
+
return Number(rows[0]?.total ?? 0);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/prompts/file.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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`;
|