nexus-ai-pro 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/API_STABILITY.md +79 -1
  2. package/CHANGELOG.md +144 -1
  3. package/README.md +10 -6
  4. package/dist/adapter-kit/gather.d.ts +76 -0
  5. package/dist/adapter-kit/gather.js +147 -0
  6. package/dist/adapter-kit/index.d.ts +1 -0
  7. package/dist/adapter-kit/index.js +1 -0
  8. package/dist/adapter-kit/record.d.ts +3 -1
  9. package/dist/adapter-kit/record.js +20 -5
  10. package/dist/agent/capabilities.d.ts +3 -0
  11. package/dist/agent/openapi.d.ts +139 -0
  12. package/dist/agent/openapi.js +455 -0
  13. package/dist/cli/adapters.d.ts +4 -3
  14. package/dist/cli/adapters.js +71 -2
  15. package/dist/cli/doctor.js +3 -0
  16. package/dist/cli.js +3 -1
  17. package/dist/core/config-builder.d.ts +1 -0
  18. package/dist/core/config-builder.js +1 -0
  19. package/dist/core/defaults.d.ts +34 -0
  20. package/dist/core/defaults.js +39 -0
  21. package/dist/decisions/completion.d.ts +10 -7
  22. package/dist/decisions/completion.js +80 -6
  23. package/dist/decisions/core.d.ts +88 -43
  24. package/dist/decisions/core.js +115 -28
  25. package/dist/decisions/errors.d.ts +25 -0
  26. package/dist/decisions/errors.js +12 -0
  27. package/dist/decisions/index.d.ts +6 -5
  28. package/dist/decisions/index.js +2 -1
  29. package/dist/decisions/integrations.d.ts +14 -5
  30. package/dist/decisions/integrations.js +16 -1
  31. package/dist/decisions/mock.d.ts +5 -3
  32. package/dist/decisions/mock.js +4 -1
  33. package/dist/decisions/pictures.d.ts +43 -0
  34. package/dist/decisions/pictures.js +103 -0
  35. package/dist/decisions/typesafe.d.ts +25 -5
  36. package/dist/decisions/typesafe.js +101 -7
  37. package/dist/doctor/index.d.ts +6 -9
  38. package/dist/doctor/index.js +41 -0
  39. package/dist/evaluate/emulate.d.ts +73 -6
  40. package/dist/evaluate/emulate.js +114 -12
  41. package/dist/operations/adapters.d.ts +1 -0
  42. package/dist/operations/adapters.js +3 -1
  43. package/dist/optimizer/densifier.js +20 -11
  44. package/dist/protocols/a2a-grpc.js +1 -1
  45. package/dist/protocols/a2a-proto.d.ts +2 -2
  46. package/dist/protocols/a2a-proto.js +76 -1
  47. package/dist/protocols/a2a.d.ts +121 -0
  48. package/dist/protocols/a2a.js +110 -32
  49. package/dist/providers/openai.js +13 -0
  50. package/dist/server/deployments.d.ts +3 -2
  51. package/dist/server/deployments.js +6 -1
  52. package/dist/tracing/stores.d.ts +2 -0
  53. package/dist/tracing/stores.js +1 -0
  54. package/dist/tracing/tracer.d.ts +5 -2
  55. package/dist/tracing/tracer.js +6 -3
  56. package/dist/types/tracing.d.ts +6 -0
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/dist-cjs/adapter-kit/gather.d.ts +76 -0
  60. package/dist-cjs/adapter-kit/gather.js +152 -0
  61. package/dist-cjs/adapter-kit/index.d.ts +1 -0
  62. package/dist-cjs/adapter-kit/index.js +4 -1
  63. package/dist-cjs/adapter-kit/record.d.ts +3 -1
  64. package/dist-cjs/adapter-kit/record.js +20 -5
  65. package/dist-cjs/agent/capabilities.d.ts +3 -0
  66. package/dist-cjs/agent/openapi.d.ts +139 -0
  67. package/dist-cjs/agent/openapi.js +461 -0
  68. package/dist-cjs/cli/adapters.d.ts +4 -3
  69. package/dist-cjs/cli/adapters.js +70 -1
  70. package/dist-cjs/cli/doctor.js +3 -0
  71. package/dist-cjs/core/config-builder.d.ts +1 -0
  72. package/dist-cjs/core/config-builder.js +5 -1
  73. package/dist-cjs/core/defaults.d.ts +34 -0
  74. package/dist-cjs/core/defaults.js +44 -0
  75. package/dist-cjs/decisions/completion.d.ts +10 -7
  76. package/dist-cjs/decisions/completion.js +79 -5
  77. package/dist-cjs/decisions/core.d.ts +88 -43
  78. package/dist-cjs/decisions/core.js +134 -46
  79. package/dist-cjs/decisions/errors.d.ts +25 -0
  80. package/dist-cjs/decisions/errors.js +16 -0
  81. package/dist-cjs/decisions/index.d.ts +6 -5
  82. package/dist-cjs/decisions/index.js +5 -1
  83. package/dist-cjs/decisions/integrations.d.ts +14 -5
  84. package/dist-cjs/decisions/integrations.js +16 -1
  85. package/dist-cjs/decisions/mock.d.ts +5 -3
  86. package/dist-cjs/decisions/mock.js +4 -1
  87. package/dist-cjs/decisions/pictures.d.ts +43 -0
  88. package/dist-cjs/decisions/pictures.js +109 -0
  89. package/dist-cjs/decisions/typesafe.d.ts +25 -5
  90. package/dist-cjs/decisions/typesafe.js +106 -12
  91. package/dist-cjs/doctor/index.d.ts +6 -9
  92. package/dist-cjs/doctor/index.js +41 -0
  93. package/dist-cjs/evaluate/emulate.d.ts +73 -6
  94. package/dist-cjs/evaluate/emulate.js +116 -13
  95. package/dist-cjs/operations/adapters.d.ts +1 -0
  96. package/dist-cjs/operations/adapters.js +3 -1
  97. package/dist-cjs/optimizer/densifier.js +20 -11
  98. package/dist-cjs/protocols/a2a-grpc.js +1 -1
  99. package/dist-cjs/protocols/a2a-proto.d.ts +2 -2
  100. package/dist-cjs/protocols/a2a-proto.js +76 -1
  101. package/dist-cjs/protocols/a2a.d.ts +121 -0
  102. package/dist-cjs/protocols/a2a.js +110 -32
  103. package/dist-cjs/providers/openai.js +13 -0
  104. package/dist-cjs/server/deployments.d.ts +3 -2
  105. package/dist-cjs/server/deployments.js +6 -1
  106. package/dist-cjs/tracing/stores.d.ts +2 -0
  107. package/dist-cjs/tracing/stores.js +1 -0
  108. package/dist-cjs/tracing/tracer.d.ts +5 -2
  109. package/dist-cjs/tracing/tracer.js +6 -3
  110. package/dist-cjs/types/tracing.d.ts +6 -0
  111. package/dist-cjs/version.d.ts +1 -1
  112. package/dist-cjs/version.js +1 -1
  113. package/llms.txt +1 -1
  114. package/package.json +14 -1
package/API_STABILITY.md CHANGED
@@ -41,6 +41,84 @@ Both are covered by this policy:
41
41
  Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
42
42
  one of its explicit subpaths.
43
43
 
44
+ ## How an item graduates
45
+
46
+ A new surface starts this way:
47
+
48
+ 1. Provider-neutral types and a deterministic mock land first.
49
+ 2. One real adapter proves the contract; conformance fixtures cover it.
50
+ 3. A second adapter on a different wire protocol proves portability.
51
+ 4. Packed-package, clean-install, and consumer type-resolution tests pass on every supported Node
52
+ line.
53
+ 5. Evaluation appropriate to the modality covers the supported capability matrix.
54
+
55
+ It leaves experimental status here only when every item below has evidence, published with the
56
+ graduation:
57
+
58
+ | Requirement | Evidence |
59
+ | --- | --- |
60
+ | API stability | Covered by the 2.x compatibility rules, with no planned breaking change |
61
+ | Conformance | Passes its contract suite on every supported backend |
62
+ | Crash recovery | Crash-injection tests at each durable boundary, where it has one |
63
+ | Concurrency | Race tests with several processes on the same records |
64
+ | Load | A published benchmark with a budget in CI |
65
+ | Tenant isolation | The isolation suite, where it stores tenant data |
66
+ | Security | A written threat review, with its findings closed |
67
+ | Upgrade | A migration test from the previous minor |
68
+ | Soak | A long-running test with flat memory and no leaked handles |
69
+
70
+ ## 2.21 stage (2.21.0)
71
+
72
+ Everything 2.21 adds is stable from the start and additive, unless this section says otherwise.
73
+
74
+ New and stable, in `nexus-ai-pro/decisions`: `BoxQuestion`, `BoxAnswer`, `DecisionBox`, `boxOverlap()`,
75
+ `statePictures()`, `readPicture()`, `DecisionPicture`, `ReadDecisionPicture`, the question type `box`
76
+ and its `DecisionDistribution`, `DecisionProviderCapabilities.images`, `answeredRight()`'s box and
77
+ `overlap`, `DecisionEvaluatorOptions.where` and `overlap`, `DecisionConformanceOptions.picture`,
78
+ `DecisionConformanceReport.skipped`, and `CompletionDecisionsOptions.images`; in
79
+ `nexus-ai-pro/decisions/typesafe`, `TypeSafeDecisionProviderOptions.media`; in `nexus-ai-pro/adapter-kit`,
80
+ `gatherRecords()`, `GatherRecordsOptions`, `GatherReport`, `GatheredSource`, `GatherOutcome`,
81
+ `RecordSource`, and `ADAPTER_KEYWORD`; the command `nexus adapters gather`; a package's
82
+ `nexus.compatibility` field, the URL of its record; in `nexus-ai-pro/config`, `useDefaults()`,
83
+ `nexusDefaults()`, `NexusDefaults`, `DEFAULT_CHANGES`, and `DefaultChange`, and the `NEXUS_DEFAULTS`
84
+ variable; `DoctorOptions.upgrade` and `nexus doctor --upgrade`; and `TraceStore.durable`. The
85
+ preview turns on exactly what 3.0 will; should 3.0 change its defaults again before it ships, the
86
+ preview follows, and `DEFAULT_CHANGES` says so. A box from TypeSafe AI rests
87
+ on what it marks as experimental, its pictures and boxes, whose request format may change between its
88
+ releases.
89
+
90
+ Changed on purpose: TypeSafe AI's 400 and 413 are `DECISION_INVALID`, not `DECISION_FAILED`; neither
91
+ was retried before or is now. A provider of your own that reads pictures declares `images: true`;
92
+ without it, a decision about a picture is not sent to it. A union over `DecisionQuestionType` or
93
+ `DecisionAnswer` that handled three kinds now meets a fourth, `box`. `compatibilityPage()` writes a
94
+ repository that is not an `http` or `https` address as text, not a link, and escapes a record's
95
+ Markdown. A doctor report has one more check, `upgrade`, skipped unless asked for. With `tenants`, an
96
+ A2A handler answers `/{tenant}/.well-known/agent-card.json` with that tenant's card, where it answered
97
+ the card itself; without `tenants`, nothing changes. A model on OpenAI's Responses API is sent
98
+ `include` and `top_logprobs` when a request asks for log probabilities, which it was not before.
99
+
100
+ ## 2.20 stage (2.20.0)
101
+
102
+ Everything 2.20 adds is stable from the start and additive, unless this section says otherwise.
103
+
104
+ New and stable:
105
+ - `nexus-ai-pro/agent/openapi`: `openApiTools()`, `loadOpenApiTools()`, `OpenApiError`,
106
+ `OpenApiDocument`, `OpenApiOperation`, `OpenApiTools`, `OpenApiToolsOptions`, and
107
+ `OpenApiHeadersRequest`;
108
+ - the capabilities `api.read`, `api.write`, and `api.destructive`;
109
+ - in `nexus-ai-pro/evaluate/emulate`: `EmulatedWorld`, `EmulatedWorldCall`, `EmulatedWorldSnapshot`,
110
+ `EmulatedToolsOptions.world` and `reads`, and `emulationKey()`'s `world`. A key without a world is
111
+ the key of every earlier release;
112
+ - in `nexus-ai-pro/protocols/a2a`: `A2aServerOptions.tenants`, `A2aTenancyOptions`,
113
+ `A2aClientOptions.tenant`, `A2aTaskRecord.tenant`, `A2aTaskQuery.tenant`, `A2A_ERRORS.permissionDenied`,
114
+ the card's `securitySchemes`, `securityRequirements`, and `signatures`, `A2aSkill.securityRequirements`,
115
+ `A2aSecurityScheme`, `A2aOAuthFlows`, `A2aSecurityRequirement`, and `A2aCardSignature`.
116
+
117
+ Changed on purpose: the gRPC binding writes and reads a card's security schemes, requirements, and
118
+ signatures, which it read past; without `tenants`, a request's tenant is still not read. A custom
119
+ `A2aTaskStore` serving several tenants should honour `A2aTaskQuery.tenant`, for full pages; the handler
120
+ filters by tenant either way.
121
+
44
122
  ## 2.19 stage (2.19.0)
45
123
 
46
124
  Everything 2.19 adds is stable from the start and additive, unless this section says otherwise.
@@ -655,7 +733,7 @@ These security fixes tighten more behavior, each closing a finding of a graduati
655
733
  ### Graduation
656
734
 
657
735
  A surface left experimental status in 2.4 only when every requirement in
658
- [How an item graduates](./ROADMAP.md#how-an-item-graduates) had evidence. That evidence is below.
736
+ [How an item graduates](#how-an-item-graduates) had evidence. That evidence is below.
659
737
  "n/a" names a requirement that does not apply, and says why. Every test named here runs in CI.
660
738
  Load and soak budgets are checked by `npm run bench:surfaces` and `npm run bench:runtime`, which are
661
739
  part of `check:release`. Upgrade tests run against the published 2.3.0 package, installed as a
package/CHANGELOG.md CHANGED
@@ -4,6 +4,145 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.21.0] - 2026-10-26
8
+
9
+ Typed decisions about pictures, compatibility records gathered from community adapters' own CI, and
10
+ 3.0's defaults behind one switch today; with log probabilities from OpenAI's Responses API, and a card
11
+ of its own for each tenant of an A2A agent.
12
+
13
+ ### Added
14
+
15
+ - **Decisions about pictures.** A decision's state may hold pictures, as image parts or image assets
16
+ anywhere in it, and a question names each by its path in backticks, such as `photo` or
17
+ `frames[2].frame`. Every question type may ask about one, and a new `box` question, `BoxQuestion`,
18
+ asks where something is: its `BoxAnswer` is a `DecisionBox` of four edges, each a fraction of the
19
+ picture's height or width, with a confidence. `boxOverlap()` measures how far two boxes overlap,
20
+ `answeredRight()` counts a box right at half or more, `calibrate()` chooses a box's threshold as any
21
+ answer's, and `decisionEvaluator()` scores a box against `where` the thing is. `statePictures()`
22
+ lists a state's pictures and `readPicture()` reads one with the media family's readers.
23
+ - TypeSafe AI's Jev is sent a state's pictures as the API's assets, each under a new reference, with
24
+ the `TypeSafe-Danger-Zone-Images` header that pictures and boxes require, and answers boxes on Jev 1.14
25
+ and later. A picture it would refuse, outside PNG or JPEG, animated, over 16 MiB, or outside 280 to
26
+ 1,568 pixels a side, or a request over 32 MiB, is refused first, naming the picture's path.
27
+ `TypeSafeDecisionProviderOptions.media` reads pictures held by file, URL, or stored asset.
28
+ - `completionDecisions()` shows a vision model the pictures a state holds, each labelled by its path,
29
+ and reads a box as the median of several samples, its confidence how far they agree; `images: false`
30
+ marks a text-only model.
31
+ - `DecisionProviderCapabilities.images`: a provider that does not read pictures is refused a decision
32
+ about one, and passed over as a fallback. The mock reads pictures and answers boxes.
33
+ - `DecisionConformanceOptions.picture` runs the conformance checks of pictures and boxes, and
34
+ `DecisionConformanceReport.skipped` lists the checks that could not run, rather than passing them.
35
+ - **Compatibility records from community CI.** An adapter's own CI publishes its sealed record at the
36
+ URL its package names as `nexus.compatibility`, beside the keyword `nexus-ai-pro-adapter`.
37
+ `nexus adapters gather`, and `gatherRecords()` in `nexus-ai-pro/adapter-kit`, find those packages on
38
+ npm, fetch each record, and keep only those whose digest matches and that name their own package,
39
+ from HTTPS; a source that is down keeps its last record, and a package that no longer names one
40
+ leaves the page. Nothing an adapter publishes is run. A daily `Community compatibility` workflow
41
+ writes the page, with the adapters this repository ships, to the `compatibility` branch, where a
42
+ record that says its adapter fails turns it red. `compatibility/sources.json` lists adapters not
43
+ on npm.
44
+
45
+ - **A preview of 3.0's defaults.** `useDefaults('3.0')`, from `nexus-ai-pro/config`, or
46
+ `NEXUS_DEFAULTS=3.0`, turns on the three settings 2.x keeps opt-in, as 3.0 will: incremental tracing on
47
+ a durable trace store, the Redis operation store's dispatch index where the client can keep it, and
48
+ `hash-v2` for a deployment first recorded. An option set explicitly wins, and without the switch
49
+ nothing changes. `DEFAULT_CHANGES` lists each, with what to do first; `nexusDefaults()` says which
50
+ defaults are in effect; and `nexus doctor --upgrade 3.0`, `DoctorOptions.upgrade`, says what 3.0
51
+ would change for a deployment. `TraceStore.durable` marks a store whose runs live only in the
52
+ process, as `MemoryTraceStore`'s.
53
+
54
+ - OpenAI's models on the Responses API, such as `gpt-5.5-pro`, report token log probabilities when a
55
+ request asks for them, as its chat models do: the request includes `message.output_text.logprobs`
56
+ with up to 20 alternatives a token, and each text part's are read back. A typed decision asked of
57
+ one is read from them in one call, rather than sampled; a model that refuses them is still sampled.
58
+
59
+ - With `tenants`, an A2A handler serves each tenant a card of its own at
60
+ `/{tenant}/.well-known/agent-card.json`, every interface naming the tenant, so a client that reads it,
61
+ such as the A2A project's reference client, calls as that tenant over each binding. The nightly
62
+ interoperability run drives a tenant through `@a2a-js/sdk` over JSON-RPC, HTTP+JSON, and gRPC, and
63
+ checks that a tenant it may not act for, and a call naming none, are refused.
64
+
65
+ ### Fixed
66
+
67
+ - The compatibility page writes a record as text: an adapter links to its repository only at an
68
+ `http` or `https` address, so a record naming `javascript:` makes no link on the HTML page; a
69
+ record's line breaks, pipes, brackets, and stars no longer break or add to the Markdown; and the
70
+ HTML page colours each row by its state.
71
+
72
+ ### Changed
73
+
74
+ - TypeSafe AI's 400 and 413 refuse a decision as `DECISION_INVALID`, as a 422 does, rather than
75
+ failing it as `DECISION_FAILED`: the request, such as a picture, is what was refused. Neither is
76
+ retried.
77
+ - A decision's trace and cache key hold a digest of each picture in its place, never its bytes, and the
78
+ budget holds a picture at 1,600 tokens rather than reading its base64 as text.
79
+ - A doctor report has one more check, `upgrade`, skipped unless `upgrade` asks about a release.
80
+
81
+ ## [2.20.2] - 2026-10-24
82
+
83
+ The first published release of 2.20. The package is 2.20.0's: the publishes of 2.20.0 and 2.20.1
84
+ stopped at a studio test that failed by chance, so neither reached npm.
85
+
86
+ ### Fixed
87
+
88
+ - The studio's load budget is judged by the fastest of five passes, after three warm-up passes
89
+ rather than one. The median of five failed the 2.20.1 publish at 3.61 calibration loops: its first
90
+ three passes took 91 to 102 ms and its last two 37 and 48 ms. A busy runner only ever adds time,
91
+ while a real slowdown slows every pass, so a request made 0.1 ms slower still fails it, at 4.3 loops.
92
+
93
+ ## [2.20.1] - 2026-10-24 (not published)
94
+
95
+ The package is 2.20.0's. Its publish stopped at the same studio test, measured another way.
96
+
97
+ ### Fixed
98
+
99
+ - The studio's load budget is measured as the median of five passes, as the surface budgets are,
100
+ rather than one: a single slow pass on a busy runner failed the 2.20.0 publish at 2.56 calibration
101
+ loops against a budget of 2.55, where the test measures 0.77. A real slowdown still fails it.
102
+
103
+ ## [2.20.0] - 2026-10-24 (not published)
104
+
105
+ Tools read from a service's OpenAPI document, an emulator that remembers the evaluation, and one A2A
106
+ handler serving many tenants.
107
+
108
+ ### Added
109
+
110
+ - **Tools from an OpenAPI document.** `openApiTools()` and `loadOpenApiTools()`, on
111
+ `nexus-ai-pro/agent/openapi`, read an OpenAPI 3.0 or 3.1 document into one tool per operation: its
112
+ name, description, and parameters the API's, its body's fields flattened, references followed, and
113
+ each declaring `api.read`, `api.write`, or `api.destructive` as its method says, with `network` for
114
+ the API's host. Given where the API lives, each tool calls it, the path encoded, the application's
115
+ headers sent, no redirect followed, and an answer read only up to a limit; a refusal is an
116
+ `OpenApiError` in the API's own words. `results` holds the schema of what each returns.
117
+ - **An emulator that remembers the evaluation.** An `EmulatedWorld`, given to `emulatedTools()` as
118
+ `world`, holds the scenario and every call across the runs of an evaluation, so a change one example
119
+ makes is there for the next; the emulator is told every change. `snapshot()` writes it as JSON,
120
+ `fork()` and `new EmulatedWorld(snapshot)` start runs from it, and the key of each answer kept carries
121
+ the world's `digest()`, so a recorded evaluation replays only after the same changes. `reads` names
122
+ tools that change nothing, beside those whose capabilities only read.
123
+ - **Capabilities for an API's operations:** `api.read:<api>/<operation>`, `api.write`, and
124
+ `api.destructive`, decided by a permission policy as MCP's are.
125
+ - **A2A for several tenants.** `a2aHandler({ tenants })` routes each request by the `tenant` it names,
126
+ in its params, its path (`/{tenant}/message:send`), or its gRPC message. Each tenant's tasks and
127
+ conversations are its own, its runs carry the tenant on their principal, and a caller that may not
128
+ act for the tenant it names is refused with `A2A_ERRORS.permissionDenied`, HTTP 403 and
129
+ `PERMISSION_DENIED` over gRPC. `A2aTenancyOptions` sets who may, and whether a tenant is required.
130
+ Task stores keep and list by tenant. A client names its interface's tenant, or its `tenant` option,
131
+ on every request, as protocol 1.0 requires.
132
+ - **A card's security.** `securitySchemes` (`A2aSecurityScheme`, with `A2aOAuthFlows`),
133
+ `securityRequirements` (`A2aSecurityRequirement`, on the card and on a skill), and `signatures`
134
+ (`A2aCardSignature`) are part of the card, and the gRPC binding writes and reads them, which it read
135
+ past before.
136
+
137
+ ### Fixed
138
+
139
+ - The prompt densifier joined a code block's fences to the text around them, so a block it meant to
140
+ keep stopped being one; it shortened phrases inside other words, turning "in order today" into
141
+ "today"; and it lowercased a phrase that began a sentence.
142
+ - A test of a call that never answers, and the unit, coverage, and studio runs, no longer let a hung
143
+ test hold the release gate: every synchronous child the tests start has a timeout, and a test that
144
+ runs past five minutes fails.
145
+
7
146
  ## [2.19.0] - 2026-10-24
8
147
 
9
148
  Typed decisions: a yes or no, a choice, or a score, answered with probabilities and a confidence read
@@ -3222,7 +3361,11 @@ shared policy vocabulary is in place for a future release that revisits this.
3222
3361
  - URL fetching rejects unsafe private-network targets and limits response reads.
3223
3362
  - CLI and audit findings no longer disclose detected secret values.
3224
3363
 
3225
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.19.0...HEAD
3364
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.21.0...HEAD
3365
+ [2.21.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.20.2...v2.21.0
3366
+ [2.20.2]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.20.1...v2.20.2
3367
+ [2.20.1]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.20.0...v2.20.1
3368
+ [2.20.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.19.0...v2.20.0
3226
3369
  [2.19.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.18.0...v2.19.0
3227
3370
  [2.18.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.17.0...v2.18.0
3228
3371
  [2.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...v2.17.0
package/README.md CHANGED
@@ -335,15 +335,19 @@ Shipped so far:
335
335
  - 2.19 typed decisions: a yes or no, a choice, or a score, answered with probabilities and a
336
336
  confidence, from TypeSafe AI's Jev or from any model's log probabilities, gated by thresholds a
337
337
  calibration chooses, so a graph's branch, an approval, or a guardrail acts when it is sure and asks
338
- a person when it is not.
338
+ a person when it is not;
339
+ - 2.20 tools read from a service's own OpenAPI document, which an agent calls or an emulator plays,
340
+ an emulator that holds what a whole evaluation did so examples that build on each other are
341
+ evaluated with nothing sent, and one A2A handler serving many tenants, each seeing only its own;
342
+ - 2.21 typed decisions about pictures, a yes or no, a choice, a score, or a box around something in
343
+ one, from TypeSafe AI's Jev or any vision model; community adapters' compatibility records gathered
344
+ from their own CI, a failing one turning red; and 3.0's defaults behind one switch today.
339
345
 
340
346
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
341
347
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
342
- Next, 2.20 reads a service's own API description into emulated tools, and lets an emulator hold
343
- what a whole evaluation did, so examples that build on each other are evaluated with nothing sent.
344
- Pinecone, images, the Gemini
345
- embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
346
- graduate once the live workflow passes against each. The
348
+ Next, in 2.22, Pinecone, images, the Gemini embedding batch tier, media understanding, video, typed
349
+ decisions, Gemini Live, SIP, and both phone providers graduate once the live workflow passes against
350
+ each. The
347
351
  [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
348
352
  applications ask for it.
349
353
 
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Compatibility records gathered from where adapters' own CI publishes them: packages found on npm by
3
+ * a keyword, each naming its record's URL in its `package.json`, and sources listed by hand. Each
4
+ * record is read as `nexus adapters verify` sealed it, and must name the package that points to it.
5
+ * Nothing an adapter publishes is run: a gathered record is listed as its CI recorded it.
6
+ */
7
+ import { type CompatibilityRecord } from './record.js';
8
+ /** The npm keyword an adapter package carries to be found: `nexus-ai-pro-adapter`. */
9
+ export declare const ADAPTER_KEYWORD = "nexus-ai-pro-adapter";
10
+ /** Where one adapter's CI publishes its record. */
11
+ export interface RecordSource {
12
+ /** The adapter's package name, which its record must name. */
13
+ package: string;
14
+ /** The record's URL: HTTPS, or HTTP to this machine. */
15
+ url: string;
16
+ }
17
+ /** Options for `gatherRecords()`. */
18
+ export interface GatherRecordsOptions {
19
+ /**
20
+ * The keyword adapter packages are found by on npm, each naming its record's URL as
21
+ * `nexus.compatibility` in its `package.json`. Defaults to `ADAPTER_KEYWORD`; `false` finds none.
22
+ */
23
+ keyword?: string | false;
24
+ /** Sources listed by hand, beside those found; a listed package's URL is used over its own. */
25
+ sources?: readonly RecordSource[];
26
+ /** The npm registry. Defaults to `https://registry.npmjs.org`. */
27
+ registry?: string;
28
+ /**
29
+ * The records gathered last time: a package whose record cannot be read now, because its source is
30
+ * down or what it serves is refused, keeps its last one, and says why.
31
+ */
32
+ previous?: readonly CompatibilityRecord[];
33
+ /** The most bytes a record may have. Defaults to 1 MiB. */
34
+ maxBytes?: number;
35
+ /** How long each request may take, in milliseconds. Defaults to 15 seconds. */
36
+ timeoutMs?: number;
37
+ /** The most packages found by keyword. Defaults to 1,000. */
38
+ maxPackages?: number;
39
+ /** Replaces the global `fetch`. */
40
+ fetch?: typeof fetch;
41
+ }
42
+ /**
43
+ * What became of one package's record: `gathered` now; `kept` from last time, as now it could not be;
44
+ * `refused` or `unreachable` with nothing to keep; or `undeclared`, a package that names no record.
45
+ */
46
+ export type GatherOutcome = 'gathered' | 'kept' | 'refused' | 'unreachable' | 'undeclared';
47
+ /** One package's source, and what became of its record. */
48
+ export interface GatheredSource {
49
+ /** The package's name. */
50
+ package: string;
51
+ /** Where its record is published, when it says. */
52
+ url?: string;
53
+ /** What became of it. */
54
+ outcome: GatherOutcome;
55
+ /** Why, for every outcome but `gathered`. */
56
+ detail?: string;
57
+ /** The record listed for it: gathered now, or kept. */
58
+ record?: CompatibilityRecord;
59
+ }
60
+ /** What `gatherRecords()` found. */
61
+ export interface GatherReport {
62
+ /** Every record to list, gathered now or kept, by package name. */
63
+ records: CompatibilityRecord[];
64
+ /** Every package, by name, and what became of its record. */
65
+ sources: GatheredSource[];
66
+ }
67
+ /**
68
+ * Gathers the compatibility records adapters' own CI publishes: finds adapter packages on npm by
69
+ * `keyword`, reads where each publishes its record, and fetches each, with `sources` listed by hand.
70
+ * A record is gathered only when it reads as sealed, with a digest that matches it, and names the
71
+ * package that points to it; a package whose record cannot be read now keeps its `previous` one.
72
+ * Throws when npm cannot be searched, rather than reporting every adapter gone.
73
+ */
74
+ export declare function gatherRecords(options?: GatherRecordsOptions): Promise<GatherReport>;
75
+ /** The name of a package from how a record names it, such as `@acme/nexus-milvus` from `@acme/nexus-milvus@0.3.0`. */
76
+ export declare function packageName(specifier: string): string;
@@ -0,0 +1,147 @@
1
+ import { readCompatibilityRecord } from './record.js';
2
+ export const ADAPTER_KEYWORD = 'nexus-ai-pro-adapter';
3
+ export async function gatherRecords(options = {}) {
4
+ const request = options.fetch ?? fetch;
5
+ const registry = (options.registry ?? 'https://registry.npmjs.org').replace(/\/+$/, '');
6
+ const timeoutMs = options.timeoutMs ?? 15_000;
7
+ const maxBytes = options.maxBytes ?? 1024 * 1024;
8
+ const get = (url) => request(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'follow' });
9
+ const found = new Map();
10
+ const keyword = options.keyword === undefined ? ADAPTER_KEYWORD : options.keyword;
11
+ if (keyword) {
12
+ for (const name of await search(get, registry, keyword, options.maxPackages ?? 1_000))
13
+ found.set(name, undefined);
14
+ }
15
+ for (const source of options.sources ?? [])
16
+ found.set(source.package, source.url);
17
+ const previous = new Map();
18
+ for (const record of options.previous ?? []) {
19
+ const name = record.adapter.package ? packageName(record.adapter.package) : undefined;
20
+ if (name)
21
+ previous.set(name, record);
22
+ }
23
+ const sources = [];
24
+ for (const name of [...found.keys()].sort()) {
25
+ const listed = found.get(name);
26
+ const result = await gatherOne(name, listed, { get, registry, maxBytes });
27
+ const last = previous.get(name);
28
+ if ((result.outcome === 'refused' || result.outcome === 'unreachable') && last) {
29
+ sources.push({ ...result, outcome: 'kept', record: last, detail: `${result.outcome}: ${result.detail}` });
30
+ }
31
+ else
32
+ sources.push(result);
33
+ }
34
+ return {
35
+ records: sources.flatMap((source) => (source.record ? [source.record] : [])),
36
+ sources,
37
+ };
38
+ }
39
+ export function packageName(specifier) {
40
+ const at = specifier.lastIndexOf('@');
41
+ return at > 0 ? specifier.slice(0, at) : specifier;
42
+ }
43
+ async function search(get, registry, keyword, limit) {
44
+ const names = [];
45
+ for (let from = 0; names.length < limit;) {
46
+ const url = `${registry}/-/v1/search?text=keywords:${encodeURIComponent(keyword)}&size=250&from=${from}`;
47
+ let page;
48
+ try {
49
+ const response = await get(url);
50
+ if (!response.ok)
51
+ throw new Error(`it answered ${response.status}`);
52
+ page = (await response.json());
53
+ }
54
+ catch (error) {
55
+ throw new Error(`npm could not be searched for "${keyword}": ${message(error)}`);
56
+ }
57
+ const objects = page.objects ?? [];
58
+ for (const item of objects)
59
+ if (typeof item.package?.name === 'string')
60
+ names.push(item.package.name);
61
+ from += objects.length;
62
+ if (objects.length === 0 || from >= (page.total ?? 0))
63
+ break;
64
+ }
65
+ return [...new Set(names)].slice(0, limit);
66
+ }
67
+ async function gatherOne(name, listed, context) {
68
+ let url = listed;
69
+ if (url === undefined) {
70
+ try {
71
+ const response = await context.get(`${context.registry}/${name.replace('/', '%2f')}/latest`);
72
+ if (!response.ok)
73
+ return { package: name, outcome: 'unreachable', detail: `npm answered ${response.status}` };
74
+ const manifest = (await response.json());
75
+ const declared = manifest.nexus?.compatibility;
76
+ if (typeof declared !== 'string' || !declared.trim()) {
77
+ return { package: name, outcome: 'undeclared', detail: 'its package.json names no nexus.compatibility record' };
78
+ }
79
+ url = declared.trim();
80
+ }
81
+ catch (error) {
82
+ return { package: name, outcome: 'unreachable', detail: `npm could not be read: ${message(error)}` };
83
+ }
84
+ }
85
+ if (!allowed(url))
86
+ return { package: name, url, outcome: 'refused', detail: `${url} is not HTTPS` };
87
+ let text;
88
+ try {
89
+ const response = await context.get(url);
90
+ if (response.url && !allowed(response.url)) {
91
+ return {
92
+ package: name,
93
+ url,
94
+ outcome: 'refused',
95
+ detail: `${url} redirects to ${response.url}, which is not HTTPS`,
96
+ };
97
+ }
98
+ if (!response.ok)
99
+ return { package: name, url, outcome: 'unreachable', detail: `${url} answered ${response.status}` };
100
+ const declared = Number(response.headers.get('content-length') ?? 0);
101
+ if (declared > context.maxBytes)
102
+ return tooLarge(name, url, declared, context.maxBytes);
103
+ text = await response.text();
104
+ }
105
+ catch (error) {
106
+ return { package: name, url, outcome: 'unreachable', detail: `${url} could not be read: ${message(error)}` };
107
+ }
108
+ const size = new TextEncoder().encode(text).length;
109
+ if (size > context.maxBytes)
110
+ return tooLarge(name, url, size, context.maxBytes);
111
+ let record;
112
+ try {
113
+ record = readCompatibilityRecord(text);
114
+ }
115
+ catch (error) {
116
+ return { package: name, url, outcome: 'refused', detail: message(error) };
117
+ }
118
+ const named = record.adapter.package;
119
+ if (!named)
120
+ return { package: name, url, outcome: 'refused', detail: 'the record names no package' };
121
+ if (packageName(named) !== name) {
122
+ return { package: name, url, outcome: 'refused', detail: `the record names ${named}, not ${name}` };
123
+ }
124
+ return { package: name, url, outcome: 'gathered', record };
125
+ }
126
+ function allowed(url) {
127
+ try {
128
+ const parsed = new URL(url);
129
+ if (parsed.protocol === 'https:')
130
+ return true;
131
+ return parsed.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname);
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ function tooLarge(name, url, size, limit) {
138
+ return {
139
+ package: name,
140
+ url,
141
+ outcome: 'refused',
142
+ detail: `the record is ${size} bytes, over the ${limit}-byte limit`,
143
+ };
144
+ }
145
+ function message(error) {
146
+ return error instanceof Error ? error.message : String(error);
147
+ }
@@ -20,6 +20,7 @@ import { type AdapterErrorClass } from './errors.js';
20
20
  import { type CompatibilityRecord, type CompatibilityRecordOptions } from './record.js';
21
21
  export { ADAPTER_CONTRACTS, ADAPTER_FIXTURE_PASSAGES, type AdapterCheck, type AdapterContract, type AdapterContractVersion, type RetrieverCapabilities, type RetrieverContractOptions, runRetrieverContract, runStoreContract, runVectorStoreContract, type StoreCapabilities, type VectorStoreCapabilities, type VectorStoreContractOptions, } from './contracts.js';
22
22
  export { AdapterError, type AdapterErrorClass, type AdapterErrorCode, classifyAdapterError } from './errors.js';
23
+ export { ADAPTER_KEYWORD, type GatheredSource, type GatherOutcome, type GatherRecordsOptions, type GatherReport, gatherRecords, type RecordSource, } from './gather.js';
23
24
  export { type CheckChange, COMPATIBILITY_RECORD_FORMAT, type CompatibilityComparison, type CompatibilityPageOptions, type CompatibilityRecord, type CompatibilityRecordAdapter, CompatibilityRecordError, type CompatibilityRecordOptions, type CompatibilityState, type CompatibilityStatus, compareRecords, compatibilityPage, compatibilityRecord, readCompatibilityRecord, recordDigest, recordStatus, } from './record.js';
24
25
  export { parseVersion, satisfiesRange } from './semver.js';
25
26
  /** The kinds of adapter the kit builds. */
@@ -7,6 +7,7 @@ import { compatibilityRecord } from './record.js';
7
7
  import { satisfiesRange } from './semver.js';
8
8
  export { ADAPTER_CONTRACTS, ADAPTER_FIXTURE_PASSAGES, runRetrieverContract, runStoreContract, runVectorStoreContract, } from './contracts.js';
9
9
  export { AdapterError, classifyAdapterError } from './errors.js';
10
+ export { ADAPTER_KEYWORD, gatherRecords, } from './gather.js';
10
11
  export { COMPATIBILITY_RECORD_FORMAT, CompatibilityRecordError, compareRecords, compatibilityPage, compatibilityRecord, readCompatibilityRecord, recordDigest, recordStatus, } from './record.js';
11
12
  export { parseVersion, satisfiesRange } from './semver.js';
12
13
  const OPERATIONS = {
@@ -145,6 +145,8 @@ export interface CompatibilityPageOptions {
145
145
  /**
146
146
  * One page listing every record: each adapter, its kind and version, the release and contract it was
147
147
  * verified against, its state there, its checks, and its p95 per operation, by kind and name. The
148
- * same records make the same page, so it can be checked for drift.
148
+ * same records make the same page, so it can be checked for drift. A record is written as text, never
149
+ * as markup: an adapter links to its repository only at an `http` or `https` address, and the HTML
150
+ * page colours each row by its state, a failing one red.
149
151
  */
150
152
  export declare function compatibilityPage(records: readonly CompatibilityRecord[], options?: CompatibilityPageOptions): string;
@@ -161,7 +161,7 @@ export function compatibilityPage(records, options = {}) {
161
161
  record,
162
162
  status,
163
163
  cells: [
164
- record.adapter.repository
164
+ record.adapter.repository && /^https?:\/\//i.test(record.adapter.repository)
165
165
  ? { text: record.adapter.name, href: record.adapter.repository }
166
166
  : { text: record.adapter.name },
167
167
  { text: record.adapter.kind },
@@ -187,7 +187,8 @@ export function compatibilityPage(records, options = {}) {
187
187
  const cell = (item) => item.href ? `<a href="${escapeHtml(item.href)}">${escapeHtml(item.text)}</a>` : escapeHtml(item.text);
188
188
  return [
189
189
  '<!doctype html>',
190
- `<html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>`,
190
+ `<html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>`,
191
+ `<style>${STATE_STYLE}</style></head><body>`,
191
192
  `<h1>${escapeHtml(title)}</h1>`,
192
193
  ...(options.intro ? [`<p>${escapeHtml(options.intro)}</p>`] : []),
193
194
  `<p>States are judged against nexus-ai-pro ${escapeHtml(nexusVersion)}.</p>`,
@@ -207,9 +208,11 @@ export function compatibilityPage(records, options = {}) {
207
208
  '',
208
209
  ].join('\n');
209
210
  }
211
+ const literal = (text) => oneLine(text).replace(/[|[\]*_`\\]/g, (char) => `\\${char}`);
210
212
  const cell = (item) => {
211
- const text = item.text.replace(/\|/g, '\\|');
212
- return item.href ? `[${text}](${item.href.replace(/\|/g, '%7C')})` : text;
213
+ const text = literal(item.text);
214
+ const href = item.href?.replace(/[|\s()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`);
215
+ return href ? `[${text}](${href})` : text;
213
216
  };
214
217
  return [
215
218
  `# ${title}`,
@@ -221,7 +224,10 @@ export function compatibilityPage(records, options = {}) {
221
224
  `| ${headers.map(() => '---').join(' | ')} |`,
222
225
  ...rows.map((row) => `| ${row.cells.map(cell).join(' | ')} |`),
223
226
  ...(notes.length > 0
224
- ? ['', ...notes.map((row) => `- **${row.record.adapter.name}**: ${row.status.reasons.join('; ')}`)]
227
+ ? [
228
+ '',
229
+ ...notes.map((row) => `- **${literal(row.record.adapter.name)}**: ${row.status.reasons.map(literal).join('; ')}`),
230
+ ]
225
231
  : []),
226
232
  '',
227
233
  ].join('\n');
@@ -240,6 +246,15 @@ function runtimeName() {
240
246
  function isRecord(value) {
241
247
  return value !== null && typeof value === 'object' && !Array.isArray(value);
242
248
  }
249
+ const STATE_STYLE = [
250
+ 'tr[data-state="passing"] td{color:#1a7f37}',
251
+ 'tr[data-state="failing"] td{color:#cf222e;font-weight:600}',
252
+ 'tr[data-state="incompatible"] td{color:#9a6700}',
253
+ 'tr[data-state="stale"] td{color:#57606a}',
254
+ ].join('');
255
+ function oneLine(text) {
256
+ return text.replace(/\s+/g, ' ').trim();
257
+ }
243
258
  function escapeHtml(text) {
244
259
  return text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
245
260
  }
@@ -12,6 +12,9 @@ import type { ToolDefinition } from '../types/messages.js';
12
12
  * | `mcp.read:<server>/<tool>` | An MCP tool its server says only reads |
13
13
  * | `mcp.write:<server>/<tool>` | An MCP tool its server says changes things but destroys nothing |
14
14
  * | `mcp.destructive:<server>/<tool>` | An MCP tool its server says may destroy or overwrite |
15
+ * | `api.read:<api>/<operation>` | An operation of an OpenAPI document whose method only reads |
16
+ * | `api.write:<api>/<operation>` | One whose method changes things |
17
+ * | `api.destructive:<api>/<operation>` | One whose method deletes |
15
18
  * | `<name>`, `<name>:<detail>` | Anything the application names, such as `payments:refund` |
16
19
  *
17
20
  * A tool that declares `capabilities: []` does nothing to the world; one that declares none at all