nexus-ai-pro 2.19.0 → 2.20.2

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.
@@ -16,6 +16,7 @@ exports.A2A_ERRORS = {
16
16
  unsupportedOperation: -32004,
17
17
  extendedCardNotConfigured: -32007,
18
18
  versionNotSupported: -32009,
19
+ permissionDenied: -32099,
19
20
  };
20
21
  class A2aError extends Error {
21
22
  code;
@@ -55,7 +56,8 @@ class MemoryA2aTaskStore {
55
56
  list(query) {
56
57
  const matching = [...this.tasks.values()]
57
58
  .filter((record) => (query.contextId === undefined || record.task.contextId === query.contextId) &&
58
- (query.state === undefined || record.task.status.state === query.state))
59
+ (query.state === undefined || record.task.status.state === query.state) &&
60
+ (query.tenant === undefined || record.tenant === query.tenant))
59
61
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
60
62
  return {
61
63
  records: matching.slice(query.offset, query.offset + query.limit).map((record) => structuredClone(record)),
@@ -94,6 +96,7 @@ function storeA2aTasks(store, options = {}) {
94
96
  const filter = {
95
97
  ...(query.contextId === undefined ? {} : { 'task.contextId': query.contextId }),
96
98
  ...(query.state === undefined ? {} : { 'task.status.state': query.state }),
99
+ ...(query.tenant === undefined ? {} : { tenant: query.tenant }),
97
100
  };
98
101
  const items = await store.search(tasks, {
99
102
  limit: query.limit,
@@ -140,6 +143,7 @@ const ERROR_STATUS = {
140
143
  [-32007]: { httpStatus: 400, grpcStatus: 'FAILED_PRECONDITION', reason: 'EXTENDED_AGENT_CARD_NOT_CONFIGURED' },
141
144
  [-32008]: { httpStatus: 400, grpcStatus: 'FAILED_PRECONDITION', reason: 'EXTENSION_SUPPORT_REQUIRED' },
142
145
  [-32009]: { httpStatus: 400, grpcStatus: 'UNIMPLEMENTED', reason: 'VERSION_NOT_SUPPORTED' },
146
+ [-32099]: { httpStatus: 403, grpcStatus: 'PERMISSION_DENIED', reason: 'PERMISSION_DENIED' },
143
147
  [-32700]: { httpStatus: 400, grpcStatus: 'INVALID_ARGUMENT', reason: 'INVALID_PARAMS' },
144
148
  [-32600]: { httpStatus: 400, grpcStatus: 'INVALID_ARGUMENT', reason: 'INVALID_PARAMS' },
145
149
  [-32601]: { httpStatus: 404, grpcStatus: 'UNIMPLEMENTED', reason: 'METHOD_NOT_FOUND' },
@@ -195,7 +199,12 @@ function a2aHandler(graph, options) {
195
199
  defaultInputModes: options.card.defaultInputModes ?? ['text/plain', 'application/json'],
196
200
  defaultOutputModes: options.card.defaultOutputModes ?? ['text/plain'],
197
201
  skills: options.card.skills,
202
+ ...(options.card.securitySchemes ? { securitySchemes: options.card.securitySchemes } : {}),
203
+ ...(options.card.securityRequirements ? { securityRequirements: options.card.securityRequirements } : {}),
204
+ ...(options.card.signatures ? { signatures: options.card.signatures } : {}),
198
205
  };
206
+ const tenancy = options.tenants ? (options.tenants === true ? {} : options.tenants) : undefined;
207
+ const conversationKey = (record) => record.tenant === undefined ? record.task.contextId : `${record.tenant}\u0000${record.task.contextId}`;
199
208
  const store = options.store ?? new MemoryA2aTaskStore({ maxTasks: options.maxTasks ?? 10_000 });
200
209
  const owner = options.lease?.owner ?? (0, shared_js_1.protocolId)('replica');
201
210
  const leaseMs = options.lease?.ms ?? 30_000;
@@ -396,8 +405,8 @@ function a2aHandler(graph, options) {
396
405
  const text = (0, shared_js_1.answerOf)(result.state);
397
406
  const reply = agentMessage(task, text);
398
407
  record.history.push(reply);
399
- const before = (await store.conversation(task.contextId)) ?? [];
400
- await store.saveConversation(task.contextId, (0, shared_js_1.transcriptOf)(result.state, [...before, (0, shared_js_1.messageOf)('assistant', text)]));
408
+ const before = (await store.conversation(conversationKey(record))) ?? [];
409
+ await store.saveConversation(conversationKey(record), (0, shared_js_1.transcriptOf)(result.state, [...before, (0, shared_js_1.messageOf)('assistant', text)]));
401
410
  task.artifacts = [{ artifactId: `${task.id}:answer`, name: 'answer', parts: [{ text }] }];
402
411
  if (streamed) {
403
412
  const last = {
@@ -437,10 +446,10 @@ function a2aHandler(graph, options) {
437
446
  }
438
447
  };
439
448
  const startRun = async (record, message, runOptions) => {
440
- const history = (await store.conversation(record.task.contextId)) ?? [];
449
+ const history = (await store.conversation(conversationKey(record))) ?? [];
441
450
  const said = (0, shared_js_1.messageOf)('user', textOfParts(message.parts));
442
451
  const result = await graph.invoke(options.input ? options.input(message, history) : { messages: [...history, said] }, runOptions);
443
- await store.saveConversation(record.task.contextId, [...history, said]);
452
+ await store.saveConversation(conversationKey(record), [...history, said]);
444
453
  return result;
445
454
  };
446
455
  const carryOn = async (record, runOptions) => {
@@ -492,11 +501,12 @@ function a2aHandler(graph, options) {
492
501
  void recover().catch((error) => options.onError?.(error));
493
502
  }, options.recoverEveryMs);
494
503
  timer?.unref?.();
495
- const taskFor = async (message, principal) => {
504
+ const taskFor = async (message, principal, mine, tenant) => {
496
505
  if (message.taskId) {
497
506
  const existing = await store.get(message.taskId);
498
- if (!existing)
507
+ if (!existing || !mine(existing)) {
499
508
  return { error: exports.A2A_ERRORS.taskNotFound, message: `Task ${message.taskId} was not found` };
509
+ }
500
510
  if (existing.task.status.state !== 'TASK_STATE_INPUT_REQUIRED') {
501
511
  return { error: exports.A2A_ERRORS.unsupportedOperation, message: `Task ${message.taskId} is not waiting for input` };
502
512
  }
@@ -515,6 +525,7 @@ function a2aHandler(graph, options) {
515
525
  history: [said],
516
526
  run: { kind: 'invoke', message: said, ...(principal ? { principal } : {}) },
517
527
  updatedAt: new Date().toISOString(),
528
+ ...(tenant !== undefined ? { tenant } : {}),
518
529
  };
519
530
  await store.put(record);
520
531
  return record;
@@ -601,9 +612,29 @@ function a2aHandler(graph, options) {
601
612
  const pushAnswer = (taskId, config, binding) => binding === 'JSONRPC' ? { taskId, ...config, pushNotificationConfig: config } : { taskId, ...config };
602
613
  const fail = (code, message) => ({ error: { code, message } });
603
614
  const call = async (method, params, context = {}) => {
604
- const principal = context.principal;
615
+ let principal = context.principal;
605
616
  const binding = context.binding ?? 'JSONRPC';
606
617
  const pushOn = Boolean(options.pushNotifications);
618
+ const named = typeof params.tenant === 'string' && params.tenant !== '' ? params.tenant : undefined;
619
+ if (tenancy) {
620
+ if (named === undefined) {
621
+ if (tenancy.required !== false)
622
+ return fail(-32602, 'This agent serves several tenants: name one as tenant');
623
+ }
624
+ else {
625
+ const allowed = tenancy.authorize ? await tenancy.authorize(named, principal) : principal?.tenantId === named;
626
+ if (!allowed)
627
+ return fail(exports.A2A_ERRORS.permissionDenied, `This caller may not act for tenant ${named}`);
628
+ if (principal?.tenantId !== named)
629
+ principal = { ...principal, tenantId: named };
630
+ }
631
+ }
632
+ const tenant = tenancy ? named : undefined;
633
+ const mine = (record) => !tenancy || record.tenant === tenant;
634
+ const own = async (id) => {
635
+ const record = await store.get(id);
636
+ return record && mine(record) ? record : undefined;
637
+ };
607
638
  try {
608
639
  switch (method) {
609
640
  case 'SendMessage':
@@ -616,7 +647,7 @@ function a2aHandler(graph, options) {
616
647
  if (pushConfig && !pushOn) {
617
648
  return fail(exports.A2A_ERRORS.pushNotificationNotSupported, 'This agent does not push notifications');
618
649
  }
619
- const record = await taskFor(message, principal);
650
+ const record = await taskFor(message, principal, mine, tenant);
620
651
  if ('error' in record)
621
652
  return fail(record.error, record.message);
622
653
  if (pushConfig) {
@@ -634,7 +665,7 @@ function a2aHandler(graph, options) {
634
665
  return { events: follow(record, () => execute(record, message, principal), context.signal) };
635
666
  }
636
667
  case 'GetTask': {
637
- const record = await store.get(String(params.id ?? ''));
668
+ const record = await own(String(params.id ?? ''));
638
669
  if (!record)
639
670
  return fail(exports.A2A_ERRORS.taskNotFound, `Task ${String(params.id)} was not found`);
640
671
  return { result: view(record, typeof params.historyLength === 'number' ? params.historyLength : undefined) };
@@ -650,6 +681,7 @@ function a2aHandler(graph, options) {
650
681
  const listed = await store.list({
651
682
  ...(typeof params.contextId === 'string' && params.contextId !== '' ? { contextId: params.contextId } : {}),
652
683
  ...(filtered ? { state: status } : {}),
684
+ ...(tenant !== undefined ? { tenant } : {}),
653
685
  limit: pageSize,
654
686
  offset,
655
687
  });
@@ -659,6 +691,7 @@ function a2aHandler(graph, options) {
659
691
  return {
660
692
  result: {
661
693
  tasks: listed.records
694
+ .filter(mine)
662
695
  .filter((record) => !after || (record.task.status.timestamp ?? '') >= after)
663
696
  .map((record) => view(record, historyLength))
664
697
  .map(({ artifacts, ...task }) => params.includeArtifacts === false ? task : { ...task, ...(artifacts ? { artifacts } : {}) }),
@@ -669,7 +702,7 @@ function a2aHandler(graph, options) {
669
702
  };
670
703
  }
671
704
  case 'SubscribeToTask': {
672
- const record = await store.get(String(params.id ?? ''));
705
+ const record = await own(String(params.id ?? ''));
673
706
  if (!record)
674
707
  return fail(exports.A2A_ERRORS.taskNotFound, `Task ${String(params.id)} was not found`);
675
708
  if (TERMINAL.has(record.task.status.state)) {
@@ -681,7 +714,7 @@ function a2aHandler(graph, options) {
681
714
  return { events: follow(record, undefined, context.signal) };
682
715
  }
683
716
  case 'CancelTask': {
684
- const record = await store.get(String(params.id ?? ''));
717
+ const record = await own(String(params.id ?? ''));
685
718
  if (!record)
686
719
  return fail(exports.A2A_ERRORS.taskNotFound, `Task ${String(params.id)} was not found`);
687
720
  if (TERMINAL.has(record.task.status.state)) {
@@ -701,7 +734,7 @@ function a2aHandler(graph, options) {
701
734
  if (!pushOn)
702
735
  return fail(exports.A2A_ERRORS.pushNotificationNotSupported, 'This agent does not push notifications');
703
736
  const taskId = String(params.taskId ?? params.id ?? '');
704
- const record = await store.get(taskId);
737
+ const record = await own(taskId);
705
738
  if (!record)
706
739
  return fail(exports.A2A_ERRORS.taskNotFound, `Task ${taskId} was not found`);
707
740
  if (method === 'CreateTaskPushNotificationConfig') {
@@ -782,8 +815,21 @@ function a2aHandler(graph, options) {
782
815
  return rpcError(id, outcome.error.code, outcome.error.message);
783
816
  return rpcResult(id, outcome.result);
784
817
  };
785
- const httpJson = async (request, route) => {
818
+ const httpJson = async (request, path) => {
786
819
  const verb = request.method;
820
+ const known = /^\/(?:message:send$|message:stream$|tasks(?:$|\/)|extendedAgentCard$)/;
821
+ let route = path;
822
+ let routedTenant;
823
+ const prefixed = /^\/([^/]+)(\/.*)$/.exec(path);
824
+ if (!known.test(path) && prefixed && known.test(prefixed[2])) {
825
+ try {
826
+ routedTenant = decodeURIComponent(prefixed[1]);
827
+ }
828
+ catch {
829
+ return restError(-32602, 'The tenant in the path is not valid percent-encoding');
830
+ }
831
+ route = prefixed[2];
832
+ }
787
833
  const query = new URL(request.url).searchParams;
788
834
  const number = (name) => (query.has(name) ? Number(query.get(name)) : undefined);
789
835
  const id = (value) => decodeURIComponent(value ?? '');
@@ -835,6 +881,8 @@ function a2aHandler(graph, options) {
835
881
  const refused = versionRefused(request);
836
882
  if (refused)
837
883
  return restError(exports.A2A_ERRORS.versionNotSupported, refused);
884
+ if (routedTenant !== undefined)
885
+ params = { ...params, tenant: routedTenant };
838
886
  if (body) {
839
887
  const text = await request.text();
840
888
  try {
@@ -1045,23 +1093,34 @@ function a2aClient(options) {
1045
1093
  });
1046
1094
  return cached;
1047
1095
  };
1048
- const endpoint = async () => isCard
1049
- ? ((await card()).supportedInterfaces.find((item) => item.protocolBinding === binding)?.url ?? options.url)
1050
- : options.url;
1096
+ const target = async () => {
1097
+ if (!isCard)
1098
+ return { url: options.url, ...(options.tenant ? { tenant: options.tenant } : {}) };
1099
+ const chosen = (await card()).supportedInterfaces.find((item) => item.protocolBinding === binding);
1100
+ const tenant = options.tenant ?? chosen?.tenant;
1101
+ return { url: chosen?.url ?? options.url, ...(tenant ? { tenant } : {}) };
1102
+ };
1051
1103
  const headers = { 'content-type': 'application/json', 'a2a-version': exports.A2A_PROTOCOL_VERSION, ...options.headers };
1052
1104
  let ids = 0;
1053
- const rpc = async (method, params, signal) => fetcher(await endpoint(), {
1054
- method: 'POST',
1055
- headers,
1056
- body: JSON.stringify({ jsonrpc: '2.0', id: ++ids, method, params }),
1057
- ...(signal ? { signal } : {}),
1058
- });
1059
- const route = async (verb, path, body, signal) => fetcher(`${(await endpoint()).replace(/\/+$/, '')}${path}`, {
1060
- method: verb,
1061
- headers: body === undefined ? { ...headers, 'content-type': 'application/json' } : headers,
1062
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
1063
- ...(signal ? { signal } : {}),
1064
- });
1105
+ const rpc = async (method, params, signal) => {
1106
+ const { url, tenant } = await target();
1107
+ return fetcher(url, {
1108
+ method: 'POST',
1109
+ headers,
1110
+ body: JSON.stringify({ jsonrpc: '2.0', id: ++ids, method, params: tenant ? { tenant, ...params } : params }),
1111
+ ...(signal ? { signal } : {}),
1112
+ });
1113
+ };
1114
+ const route = async (verb, path, body, signal) => {
1115
+ const { url, tenant } = await target();
1116
+ const under = tenant ? `/${encodeURIComponent(tenant)}` : '';
1117
+ return fetcher(`${url.replace(/\/+$/, '')}${under}${path}`, {
1118
+ method: verb,
1119
+ headers: body === undefined ? { ...headers, 'content-type': 'application/json' } : headers,
1120
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
1121
+ ...(signal ? { signal } : {}),
1122
+ });
1123
+ };
1065
1124
  const restFailure = async (response) => {
1066
1125
  const body = (await response.json().catch(() => ({})));
1067
1126
  const reason = body.error?.details?.find((detail) => detail.reason)?.reason;
@@ -2,4 +2,4 @@
2
2
  * This package's version, recorded where provenance matters, such as on every experiment. A test
3
3
  * keeps it equal to `package.json`.
4
4
  */
5
- export declare const NEXUS_VERSION = "2.19.0";
5
+ export declare const NEXUS_VERSION = "2.20.2";
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NEXUS_VERSION = void 0;
4
- exports.NEXUS_VERSION = '2.19.0';
4
+ exports.NEXUS_VERSION = '2.20.2';
package/llms.txt CHANGED
@@ -5,7 +5,7 @@
5
5
  Import only what you use. The root import is the client and nothing else, and every other capability has its own entry point with a size budget CI enforces; [the packaging guide](https://raw.githubusercontent.com/mkhitar-abrahamyan/nexus-ai/main/docs/packaging.md) publishes what each one costs. The package has no required dependencies: a graph-only application loads 81 KB, and installs nothing but this package. The client, the provider adapters, graphs, agents, and the agent protocols run on Node.js, Deno, Bun, edge runtimes, and browsers; [the runtimes guide](https://raw.githubusercontent.com/mkhitar-abrahamyan/nexus-ai/main/docs/runtimes.md) lists the entry points that need Node.js, and why.
6
6
 
7
7
  - Install with `npm install nexus-ai-pro`. It needs Node.js 22 or newer, and ships ESM and CommonJS builds with shared types. License: MIT.
8
- - It has 198 entry points. `nexus-ai-pro` itself is the client, and every other capability has its own subpath, such as `nexus-ai-pro/graph`.
8
+ - It has 199 entry points. `nexus-ai-pro` itself is the client, and every other capability has its own subpath, such as `nexus-ai-pro/graph`.
9
9
  - Nothing is a required dependency. Install only the optional peers you use: `@anthropic-ai/sdk`, `@types/node`, `ajv`, `ajv-formats`, `ollama`, `openai`, `zod`.
10
10
  - Each guide explains every export of its entry points, then lists them in a reference generated from the doc comments.
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-ai-pro",
3
- "version": "2.19.0",
3
+ "version": "2.20.2",
4
4
  "description": "Typed AI framework for TypeScript: provider routing and failover, durable agent graphs and workflows, retrieval over any vector store, MCP, evals, guardrails, and cost control",
5
5
  "type": "module",
6
6
  "main": "dist-cjs/index.js",
@@ -670,6 +670,16 @@
670
670
  "default": "./dist-cjs/agent/middleware/index.js"
671
671
  }
672
672
  },
673
+ "./agent/openapi": {
674
+ "import": {
675
+ "types": "./dist/agent/openapi.d.ts",
676
+ "default": "./dist/agent/openapi.js"
677
+ },
678
+ "require": {
679
+ "types": "./dist-cjs/agent/openapi.d.ts",
680
+ "default": "./dist-cjs/agent/openapi.js"
681
+ }
682
+ },
673
683
  "./deep-agent": {
674
684
  "import": {
675
685
  "types": "./dist/deep-agent/index.d.ts",
@@ -2412,6 +2422,9 @@
2412
2422
  "agent/middleware": [
2413
2423
  "dist-cjs/agent/middleware/index.d.ts"
2414
2424
  ],
2425
+ "agent/openapi": [
2426
+ "dist-cjs/agent/openapi.d.ts"
2427
+ ],
2415
2428
  "deep-agent": [
2416
2429
  "dist-cjs/deep-agent/index.d.ts"
2417
2430
  ],