nexus-ai-pro 2.4.0 → 2.6.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 +121 -0
- package/CHANGELOG.md +248 -1
- package/README.md +14 -3
- package/SECURITY.md +78 -9
- package/dist/agent/create-agent.d.ts +37 -2
- package/dist/agent/create-agent.js +62 -4
- package/dist/agent/index.d.ts +2 -1
- package/dist/agent/tool.d.ts +19 -4
- package/dist/agent/tool.js +27 -4
- 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/doctor/index.d.ts +4 -2
- package/dist/doctor/index.js +29 -0
- package/dist/evaluate/threads.d.ts +187 -0
- package/dist/evaluate/threads.js +187 -0
- package/dist/graph/event-stream.js +2 -0
- package/dist/graph/graph.js +4 -1
- package/dist/mcp/client.d.ts +9 -1
- package/dist/mcp/client.js +38 -8
- package/dist/mcp/server.js +42 -2
- 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/protocols/a2a.d.ts +166 -1
- package/dist/protocols/a2a.js +499 -132
- package/dist/protocols/acp-editor.d.ts +43 -0
- package/dist/protocols/acp-editor.js +164 -0
- package/dist/protocols/acp.d.ts +76 -3
- package/dist/protocols/acp.js +180 -7
- package/dist/protocols/ag-ui.d.ts +23 -0
- package/dist/protocols/ag-ui.js +128 -8
- package/dist/server/deployments.d.ts +127 -12
- package/dist/server/deployments.js +402 -37
- package/dist/server/metrics.d.ts +2 -0
- package/dist/server/metrics.js +7 -0
- package/dist/server/runs.d.ts +9 -1
- package/dist/server/runs.js +46 -7
- package/dist/server/server.js +4 -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/tracing/instrument.js +15 -3
- package/dist/tracing/tracer.d.ts +4 -0
- package/dist/tracing/tracer.js +6 -0
- package/dist/types/graph.d.ts +7 -0
- package/dist/types/messages.d.ts +12 -0
- package/dist/types/operations.d.ts +7 -0
- package/dist/types/server.d.ts +61 -2
- package/dist/types/store.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/agent/create-agent.d.ts +37 -2
- package/dist-cjs/agent/create-agent.js +62 -4
- package/dist-cjs/agent/index.d.ts +2 -1
- package/dist-cjs/agent/tool.d.ts +19 -4
- package/dist-cjs/agent/tool.js +27 -4
- 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/doctor/index.d.ts +4 -2
- package/dist-cjs/doctor/index.js +29 -0
- package/dist-cjs/evaluate/threads.d.ts +187 -0
- package/dist-cjs/evaluate/threads.js +198 -0
- package/dist-cjs/graph/event-stream.js +2 -0
- package/dist-cjs/graph/graph.js +4 -1
- package/dist-cjs/mcp/client.d.ts +9 -1
- package/dist-cjs/mcp/client.js +37 -7
- package/dist-cjs/mcp/server.js +42 -2
- 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/protocols/a2a.d.ts +166 -1
- package/dist-cjs/protocols/a2a.js +501 -132
- package/dist-cjs/protocols/acp-editor.d.ts +43 -0
- package/dist-cjs/protocols/acp-editor.js +167 -0
- package/dist-cjs/protocols/acp.d.ts +76 -3
- package/dist-cjs/protocols/acp.js +183 -8
- package/dist-cjs/protocols/ag-ui.d.ts +23 -0
- package/dist-cjs/protocols/ag-ui.js +127 -7
- package/dist-cjs/server/deployments.d.ts +127 -12
- package/dist-cjs/server/deployments.js +402 -37
- package/dist-cjs/server/metrics.d.ts +2 -0
- package/dist-cjs/server/metrics.js +8 -0
- package/dist-cjs/server/runs.d.ts +9 -1
- package/dist-cjs/server/runs.js +45 -6
- package/dist-cjs/server/server.js +4 -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/tracing/instrument.js +15 -3
- package/dist-cjs/tracing/tracer.d.ts +4 -0
- package/dist-cjs/tracing/tracer.js +6 -0
- package/dist-cjs/types/graph.d.ts +7 -0
- package/dist-cjs/types/messages.d.ts +12 -0
- package/dist-cjs/types/operations.d.ts +7 -0
- package/dist-cjs/types/server.d.ts +61 -2
- package/dist-cjs/types/store.d.ts +9 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +43 -2
package/dist/mcp/server.js
CHANGED
|
@@ -66,10 +66,37 @@ export class McpServer {
|
|
|
66
66
|
case 'tools/call': {
|
|
67
67
|
const name = String(params.name ?? '');
|
|
68
68
|
const tool = this.tools.get(name);
|
|
69
|
-
if (!tool?.execute)
|
|
69
|
+
if (!tool?.execute && !tool?.stream)
|
|
70
70
|
return { content: textContent(`Unknown tool "${name}"`), isError: true };
|
|
71
|
+
const args = (params.arguments ?? {});
|
|
72
|
+
const token = params._meta?.progressToken;
|
|
73
|
+
let reports = 0;
|
|
74
|
+
const progress = typeof token === 'string' || typeof token === 'number'
|
|
75
|
+
? (value) => {
|
|
76
|
+
reports += 1;
|
|
77
|
+
void this.transport?.send({
|
|
78
|
+
jsonrpc: '2.0',
|
|
79
|
+
method: 'notifications/progress',
|
|
80
|
+
params: { progressToken: token, ...progressParams(value, reports) },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
: undefined;
|
|
71
84
|
try {
|
|
72
|
-
|
|
85
|
+
let output;
|
|
86
|
+
if (tool.stream) {
|
|
87
|
+
const generator = tool.stream(args, progress ? { progress } : {});
|
|
88
|
+
for (;;) {
|
|
89
|
+
const step = await generator.next();
|
|
90
|
+
if (step.done) {
|
|
91
|
+
output = step.value;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
progress?.(step.value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
output = await tool.execute(args, progress ? { progress } : {});
|
|
99
|
+
}
|
|
73
100
|
return { content: textContent(output) };
|
|
74
101
|
}
|
|
75
102
|
catch (error) {
|
|
@@ -136,3 +163,16 @@ export function createStdioServerTransport(streams) {
|
|
|
136
163
|
},
|
|
137
164
|
};
|
|
138
165
|
}
|
|
166
|
+
function progressParams(value, position) {
|
|
167
|
+
if (typeof value === 'number')
|
|
168
|
+
return { progress: value };
|
|
169
|
+
if (value && typeof value === 'object') {
|
|
170
|
+
const report = value;
|
|
171
|
+
return {
|
|
172
|
+
progress: typeof report.progress === 'number' ? report.progress : position,
|
|
173
|
+
...(typeof report.total === 'number' ? { total: report.total } : {}),
|
|
174
|
+
message: typeof report.message === 'string' ? report.message : JSON.stringify(value),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { progress: position, message: String(value) };
|
|
178
|
+
}
|
|
@@ -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/protocols/a2a.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Message, ToolDefinition } from '../types/messages.js';
|
|
2
2
|
import type { Principal } from '../types/principal.js';
|
|
3
|
+
import type { Store } from '../types/store.js';
|
|
3
4
|
import { type ProtocolGraph, type ProtocolInterrupt } from './shared.js';
|
|
4
5
|
export type { ProtocolGraph, ProtocolInterrupt, ProtocolResult, ProtocolRunOptions } from './shared.js';
|
|
5
6
|
/** The protocol version this module speaks. */
|
|
@@ -137,7 +138,9 @@ export type A2aStreamEvent = {
|
|
|
137
138
|
export declare const A2A_ERRORS: {
|
|
138
139
|
readonly taskNotFound: -32001;
|
|
139
140
|
readonly taskNotCancelable: -32002;
|
|
141
|
+
readonly pushNotificationNotSupported: -32003;
|
|
140
142
|
readonly unsupportedOperation: -32004;
|
|
143
|
+
readonly extendedCardNotConfigured: -32007;
|
|
141
144
|
readonly versionNotSupported: -32009;
|
|
142
145
|
};
|
|
143
146
|
/** Raised by the client when an agent answers with a JSON-RPC error. */
|
|
@@ -152,6 +155,109 @@ export declare class A2aError extends Error {
|
|
|
152
155
|
/** The error's data, when the agent sent any. */
|
|
153
156
|
data?: unknown | undefined);
|
|
154
157
|
}
|
|
158
|
+
/** Where a task's updates are pushed: a webhook the calling agent gave. */
|
|
159
|
+
export interface A2aPushNotificationConfig {
|
|
160
|
+
/** Its id, unique within the task. Given one when it has none. */
|
|
161
|
+
id?: string;
|
|
162
|
+
/** Where each update is POSTed. */
|
|
163
|
+
url: string;
|
|
164
|
+
/** Sent back as the `X-A2A-Notification-Token` header, so the receiver knows the update is its own. */
|
|
165
|
+
token?: string;
|
|
166
|
+
}
|
|
167
|
+
/** A task as a store keeps it: the task, its messages, the questions it waits on, and its webhooks. */
|
|
168
|
+
export interface A2aTaskRecord {
|
|
169
|
+
/** The task. */
|
|
170
|
+
task: A2aTask;
|
|
171
|
+
/** Every message of the task, the caller's and the agent's. */
|
|
172
|
+
history: A2aMessage[];
|
|
173
|
+
/** The questions the paused run asked, which the next message answers. */
|
|
174
|
+
interrupts?: ProtocolInterrupt[];
|
|
175
|
+
/** Where its updates are pushed. */
|
|
176
|
+
push?: A2aPushNotificationConfig[];
|
|
177
|
+
/** ISO-8601 time it was last written. */
|
|
178
|
+
updatedAt: string;
|
|
179
|
+
}
|
|
180
|
+
/** Which tasks `A2aTaskStore.list()` returns. */
|
|
181
|
+
export interface A2aTaskQuery {
|
|
182
|
+
/** Only this conversation's. */
|
|
183
|
+
contextId?: string;
|
|
184
|
+
/** Only those in this state. */
|
|
185
|
+
state?: A2aTaskState;
|
|
186
|
+
/** Most returned. */
|
|
187
|
+
limit: number;
|
|
188
|
+
/** Skipped first, for paging. */
|
|
189
|
+
offset: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Where an A2A agent keeps its tasks and conversations. A store outside the process lets a restart, or
|
|
193
|
+
* another replica, pick a task up: answer a task's question, read it, list it, or cancel it.
|
|
194
|
+
*/
|
|
195
|
+
export interface A2aTaskStore {
|
|
196
|
+
/** A task's record. */
|
|
197
|
+
get(taskId: string): Promise<A2aTaskRecord | undefined> | A2aTaskRecord | undefined;
|
|
198
|
+
/** Writes a task's record. */
|
|
199
|
+
put(record: A2aTaskRecord): Promise<void> | void;
|
|
200
|
+
/** Tasks, most recently written first, and how many match when the store knows. */
|
|
201
|
+
list(query: A2aTaskQuery): Promise<{
|
|
202
|
+
records: A2aTaskRecord[];
|
|
203
|
+
total?: number;
|
|
204
|
+
}> | {
|
|
205
|
+
records: A2aTaskRecord[];
|
|
206
|
+
total?: number;
|
|
207
|
+
};
|
|
208
|
+
/** A conversation's transcript, as the agent last left it. */
|
|
209
|
+
conversation(contextId: string): Promise<Message[] | undefined> | Message[] | undefined;
|
|
210
|
+
/** Writes a conversation's transcript. */
|
|
211
|
+
saveConversation(contextId: string, messages: Message[]): Promise<void> | void;
|
|
212
|
+
}
|
|
213
|
+
/** Tasks and conversations in this process: the default, for one replica. */
|
|
214
|
+
export declare class MemoryA2aTaskStore implements A2aTaskStore {
|
|
215
|
+
private readonly tasks;
|
|
216
|
+
private readonly conversations;
|
|
217
|
+
private readonly max;
|
|
218
|
+
constructor(options?: {
|
|
219
|
+
maxTasks?: number;
|
|
220
|
+
});
|
|
221
|
+
/** A task's record, as a copy. */
|
|
222
|
+
get(taskId: string): A2aTaskRecord | undefined;
|
|
223
|
+
/** Writes a task's record, dropping the least recently written beyond `maxTasks`. */
|
|
224
|
+
put(record: A2aTaskRecord): void;
|
|
225
|
+
/** Tasks, most recently written first, with how many match. */
|
|
226
|
+
list(query: A2aTaskQuery): {
|
|
227
|
+
records: A2aTaskRecord[];
|
|
228
|
+
total: number;
|
|
229
|
+
};
|
|
230
|
+
/** A conversation's transcript. */
|
|
231
|
+
conversation(contextId: string): Message[] | undefined;
|
|
232
|
+
/** Writes a conversation's transcript. */
|
|
233
|
+
saveConversation(contextId: string, messages: Message[]): void;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Tasks and conversations in any long-term `Store`: the memory, Redis, Postgres, or SQLite store, so
|
|
237
|
+
* every replica sees one set. Listing filters on the task's fields in the store; it does not count
|
|
238
|
+
* all matches, so `total` is unknown.
|
|
239
|
+
*/
|
|
240
|
+
export declare function storeA2aTasks(store: Store, options?: {
|
|
241
|
+
namespace?: readonly string[];
|
|
242
|
+
}): A2aTaskStore;
|
|
243
|
+
/** Options for pushing task updates to the webhooks callers give. */
|
|
244
|
+
export interface A2aPushOptions {
|
|
245
|
+
/**
|
|
246
|
+
* Signs every delivery with an \`x-nexus-signature\` header, as the server's operation webhooks are,
|
|
247
|
+
* so a receiver checks it with \`verifyOperationWebhook()\` from \`nexus-ai-pro/operations\`.
|
|
248
|
+
*/
|
|
249
|
+
secret?: string;
|
|
250
|
+
/**
|
|
251
|
+
* Allows webhooks on loopback, private, and link-local addresses, and plain \`http\`. Off by default,
|
|
252
|
+
* so a caller cannot make the agent reach into its own network. Turn it on for tests, or when every
|
|
253
|
+
* caller is trusted.
|
|
254
|
+
*/
|
|
255
|
+
allowPrivateNetworks?: boolean;
|
|
256
|
+
/** Replaces \`fetch\` for deliveries: a client with its own address checks, a proxy, or a test. */
|
|
257
|
+
fetch?: typeof globalThis.fetch;
|
|
258
|
+
/** How long one delivery may take, in milliseconds. Defaults to 5 seconds. */
|
|
259
|
+
timeoutMs?: number;
|
|
260
|
+
}
|
|
155
261
|
/** Options for `a2aHandler()`. */
|
|
156
262
|
export interface A2aServerOptions {
|
|
157
263
|
/** The card's content: the endpoint `url` is where this handler is mounted. */
|
|
@@ -170,8 +276,25 @@ export interface A2aServerOptions {
|
|
|
170
276
|
answer?: (message: A2aMessage, interrupt: ProtocolInterrupt) => unknown;
|
|
171
277
|
/** Who the task is for, from the request: the place to authenticate the calling agent. */
|
|
172
278
|
principal?: (request: Request) => Principal | undefined | Promise<Principal | undefined>;
|
|
173
|
-
/**
|
|
279
|
+
/**
|
|
280
|
+
* Where tasks and conversations are kept. Defaults to memory, for one replica; \`storeA2aTasks()\`
|
|
281
|
+
* keeps them in a shared store. A task resumed elsewhere needs the graph's own checkpointer to be
|
|
282
|
+
* shared too.
|
|
283
|
+
*/
|
|
284
|
+
store?: A2aTaskStore;
|
|
285
|
+
/** Most tasks and contexts the default memory store keeps. Defaults to 10,000 each. */
|
|
174
286
|
maxTasks?: number;
|
|
287
|
+
/** Pushes each task's updates to the webhooks callers give. Without it, push notifications are not offered. */
|
|
288
|
+
pushNotifications?: A2aPushOptions;
|
|
289
|
+
/**
|
|
290
|
+
* The extended card an authenticated caller may read with `GetExtendedAgentCard`: more skills, or
|
|
291
|
+
* anything else this caller may see. Without it, there is no extended card.
|
|
292
|
+
*/
|
|
293
|
+
extendedCard?: (principal: Principal) => Partial<A2aAgentCard> | Promise<Partial<A2aAgentCard>>;
|
|
294
|
+
/** How often `SubscribeToTask` reads a task another replica runs, in milliseconds. Defaults to 1 second. */
|
|
295
|
+
pollMs?: number;
|
|
296
|
+
/** Receives push delivery failures, which never fail a task. */
|
|
297
|
+
onError?: (error: unknown) => void;
|
|
175
298
|
}
|
|
176
299
|
/**
|
|
177
300
|
* An HTTP handler that serves a graph as an A2A agent: `GET` the card, `POST` JSON-RPC. Mount it
|
|
@@ -201,21 +324,63 @@ export interface A2aSendOptions {
|
|
|
201
324
|
taskId?: string;
|
|
202
325
|
/** Structured data sent beside the text, such as `{ approved: true }`. */
|
|
203
326
|
data?: unknown;
|
|
327
|
+
/** A webhook for this task's updates, when the agent pushes them. */
|
|
328
|
+
push?: A2aPushNotificationConfig;
|
|
329
|
+
/** Returns the task while it runs, instead of when it is done. */
|
|
330
|
+
returnImmediately?: boolean;
|
|
204
331
|
/** Cancels the request. */
|
|
205
332
|
signal?: AbortSignal;
|
|
206
333
|
}
|
|
334
|
+
/** Which tasks `A2aClient.listTasks()` asks for. */
|
|
335
|
+
export interface A2aListTasksOptions {
|
|
336
|
+
/** Only this conversation's. */
|
|
337
|
+
contextId?: string;
|
|
338
|
+
/** Only those in this state. */
|
|
339
|
+
status?: A2aTaskState;
|
|
340
|
+
/** Tasks per page, up to 100. Defaults to 50. */
|
|
341
|
+
pageSize?: number;
|
|
342
|
+
/** The `nextPageToken` of the page before. */
|
|
343
|
+
pageToken?: string;
|
|
344
|
+
/** Messages of each task's history to include. */
|
|
345
|
+
historyLength?: number;
|
|
346
|
+
}
|
|
347
|
+
/** A page of tasks. */
|
|
348
|
+
export interface A2aTaskPage {
|
|
349
|
+
/** The tasks. */
|
|
350
|
+
tasks: A2aTask[];
|
|
351
|
+
/** The token for the next page, or empty when this is the last. */
|
|
352
|
+
nextPageToken: string;
|
|
353
|
+
/** The page size used. */
|
|
354
|
+
pageSize: number;
|
|
355
|
+
/** How many tasks match, when the agent's store counts them. */
|
|
356
|
+
totalSize?: number;
|
|
357
|
+
}
|
|
207
358
|
/** A client for one A2A agent. */
|
|
208
359
|
export interface A2aClient {
|
|
209
360
|
/** The agent's card, fetched once. */
|
|
210
361
|
card(): Promise<A2aAgentCard>;
|
|
362
|
+
/** The card for an authenticated caller, as `GetExtendedAgentCard` returns it. */
|
|
363
|
+
extendedCard(): Promise<A2aAgentCard>;
|
|
211
364
|
/** Sends a message and waits for the task it starts or continues. */
|
|
212
365
|
send(text: string, options?: A2aSendOptions): Promise<A2aTask>;
|
|
213
366
|
/** Sends a message and reads the task's events as they arrive. */
|
|
214
367
|
stream(text: string, options?: A2aSendOptions): AsyncGenerator<A2aStreamEvent>;
|
|
215
368
|
/** Reads a task. */
|
|
216
369
|
getTask(taskId: string, historyLength?: number): Promise<A2aTask>;
|
|
370
|
+
/** Lists tasks, a page at a time. */
|
|
371
|
+
listTasks(options?: A2aListTasksOptions): Promise<A2aTaskPage>;
|
|
372
|
+
/** Follows a task that is still running, from now until it finishes or waits. */
|
|
373
|
+
subscribe(taskId: string, options?: {
|
|
374
|
+
signal?: AbortSignal;
|
|
375
|
+
}): AsyncGenerator<A2aStreamEvent>;
|
|
217
376
|
/** Cancels a task. */
|
|
218
377
|
cancel(taskId: string): Promise<A2aTask>;
|
|
378
|
+
/** Adds a webhook for a task's updates. */
|
|
379
|
+
setPushNotification(taskId: string, config: A2aPushNotificationConfig): Promise<A2aPushNotificationConfig>;
|
|
380
|
+
/** A task's webhooks. */
|
|
381
|
+
listPushNotifications(taskId: string): Promise<A2aPushNotificationConfig[]>;
|
|
382
|
+
/** Removes one of a task's webhooks. */
|
|
383
|
+
deletePushNotification(taskId: string, configId: string): Promise<void>;
|
|
219
384
|
}
|
|
220
385
|
/** A client for an A2A agent, by its card or endpoint URL. */
|
|
221
386
|
export declare function a2aClient(options: A2aClientOptions): A2aClient;
|