nexus-ai-pro 2.17.0 → 2.18.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 +35 -0
- package/CHANGELOG.md +78 -1
- package/README.md +7 -3
- package/dist/evaluate/media.d.ts +6 -2
- package/dist/evaluate/media.js +7 -21
- package/dist/index.d.ts +1 -1
- package/dist/mcp/client.d.ts +25 -2
- package/dist/mcp/client.js +82 -47
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +4 -1
- package/dist/protocols/acp-editor.js +42 -15
- package/dist/router/failover.js +19 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/list-page.d.ts +38 -0
- package/dist/server/list-page.js +49 -0
- package/dist/server/regions.d.ts +15 -1
- package/dist/server/regions.js +70 -0
- package/dist/server/runs.d.ts +24 -0
- package/dist/server/runs.js +18 -4
- package/dist/server/server.js +17 -5
- package/dist/tracing/index.d.ts +2 -2
- package/dist/tracing/instrument.d.ts +7 -2
- package/dist/tracing/instrument.js +69 -2
- package/dist/tracing/otlp-proto.js +18 -0
- package/dist/tracing/otlp.d.ts +26 -1
- package/dist/tracing/otlp.js +19 -0
- package/dist/tracing/tracer.d.ts +36 -1
- package/dist/tracing/tracer.js +68 -2
- package/dist/types/response.d.ts +18 -0
- package/dist/types/tracing.d.ts +32 -0
- package/dist/utils/media-duration.d.ts +20 -0
- package/dist/utils/media-duration.js +334 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/video/google.d.ts +8 -0
- package/dist/video/google.js +31 -9
- package/dist/video/index.d.ts +18 -3
- package/dist/video/index.js +46 -3
- package/dist/video/mock.js +3 -1
- package/dist/video/runway.d.ts +2 -0
- package/dist/video/runway.js +45 -11
- package/dist-cjs/evaluate/media.d.ts +6 -2
- package/dist-cjs/evaluate/media.js +9 -21
- package/dist-cjs/index.d.ts +1 -1
- package/dist-cjs/mcp/client.d.ts +25 -2
- package/dist-cjs/mcp/client.js +82 -47
- package/dist-cjs/mcp/server.d.ts +1 -1
- package/dist-cjs/mcp/server.js +4 -1
- package/dist-cjs/protocols/acp-editor.js +42 -15
- package/dist-cjs/router/failover.js +19 -1
- package/dist-cjs/server/index.d.ts +1 -0
- package/dist-cjs/server/list-page.d.ts +38 -0
- package/dist-cjs/server/list-page.js +56 -0
- package/dist-cjs/server/regions.d.ts +15 -1
- package/dist-cjs/server/regions.js +70 -0
- package/dist-cjs/server/runs.d.ts +24 -0
- package/dist-cjs/server/runs.js +18 -4
- package/dist-cjs/server/server.js +17 -5
- package/dist-cjs/tracing/index.d.ts +2 -2
- package/dist-cjs/tracing/instrument.d.ts +7 -2
- package/dist-cjs/tracing/instrument.js +69 -2
- package/dist-cjs/tracing/otlp-proto.js +18 -0
- package/dist-cjs/tracing/otlp.d.ts +26 -1
- package/dist-cjs/tracing/otlp.js +19 -0
- package/dist-cjs/tracing/tracer.d.ts +36 -1
- package/dist-cjs/tracing/tracer.js +68 -2
- package/dist-cjs/types/response.d.ts +18 -0
- package/dist-cjs/types/tracing.d.ts +32 -0
- package/dist-cjs/utils/media-duration.d.ts +20 -0
- package/dist-cjs/utils/media-duration.js +337 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/dist-cjs/video/google.d.ts +8 -0
- package/dist-cjs/video/google.js +31 -9
- package/dist-cjs/video/index.d.ts +18 -3
- package/dist-cjs/video/index.js +46 -3
- package/dist-cjs/video/mock.js +3 -1
- package/dist-cjs/video/runway.d.ts +2 -0
- package/dist-cjs/video/runway.js +45 -11
- package/package.json +1 -1
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,41 @@ 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
|
+
## 2.18 stage (2.18.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.18 adds is stable from the start and additive, unless this section says otherwise.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- in `nexus-ai-pro/tracing`: `RunEvent`, `RunLink`, `RunAttributeValue`, `RunEventInput`, `RunLinkInput`,
|
|
50
|
+
`Run.events`, `droppedEvents`, and `links`, `Tracer.event()`, `RunHandle.event()` and `link()`,
|
|
51
|
+
`StartRunOptions.links`, `FinishRunOptions.events` and `links`, `TracerOptions.maxEventsPerRun`, and
|
|
52
|
+
the event names `retry`, `failover`, `progress`, `guardrail`, `cache`, and `capability_warning`;
|
|
53
|
+
- in `nexus-ai-pro/tracing/otlp`: `OtlpSpanEvent`, `OtlpSpanLink`, and `OtlpSpan.events`,
|
|
54
|
+
`droppedEventsCount`, and `links`;
|
|
55
|
+
- in the root: `ResponseAttempt` and `ResponseMeta.failedAttempts`;
|
|
56
|
+
- in `nexus-ai-pro/evaluate/media`: `readMediaDuration()` and `MediaDuration`;
|
|
57
|
+
- in `nexus-ai-pro/video`, experimental with the family: `VideoModelCapabilities.maxInputVideoSeconds`;
|
|
58
|
+
- in `nexus-ai-pro/server/regions`: `RegionalGatewayOptions.lists`, `RegionListGap`, and a merged
|
|
59
|
+
list's `regions`, `incomplete`, and `nextCursor`;
|
|
60
|
+
- in `nexus-ai-pro/server`: `ListPage`, `RunManager.listThreads()` and `listRuns()`, and the lists'
|
|
61
|
+
`cursor` and `nextCursor`;
|
|
62
|
+
- in `nexus-ai-pro/mcp`: `McpClientOptions.reconnect`, `McpClient.onToolsChanged()`, and
|
|
63
|
+
`SseClientOptions.reopen`;
|
|
64
|
+
- in `nexus-ai-pro/video`, experimental with the family: `VideoGenerateRequest.referenceVideos` and
|
|
65
|
+
`VideoProviderRequest.referenceVideos`, the `reference-videos` input kind,
|
|
66
|
+
`VideoModelCapabilities.maxReferenceVideos` and `referenceVideoSeconds`, and
|
|
67
|
+
`VideoConformanceOptions.referenceVideo`;
|
|
68
|
+
- in `nexus-ai-pro/video/google`, experimental with the family:
|
|
69
|
+
`GoogleVideoProviderConfig.interactionsBaseUrl`, and Gemini Omni Flash on Vertex AI.
|
|
70
|
+
|
|
71
|
+
Changed on purpose:
|
|
72
|
+
- a list through the regional gateway is merged from every region the tenant may reach;
|
|
73
|
+
`lists: 'home'` keeps the earlier answer;
|
|
74
|
+
- an agent server's lists are ordered by last write, then id, and a `limit` that is not a whole
|
|
75
|
+
number above 0 is refused;
|
|
76
|
+
- `McpServer` says `tools.listChanged`, and `register()` sends the notification;
|
|
77
|
+
- `VideoInputKind` widens with `reference-videos`.
|
|
78
|
+
|
|
44
79
|
## 2.17 stage (2.17.0)
|
|
45
80
|
|
|
46
81
|
Everything 2.17 adds is stable from the start and additive, unless this section says otherwise.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,82 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.18.0] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
Span events and links, media that knows its length, a tenant's threads and runs listed and paged
|
|
10
|
+
across all of its regions, MCP servers that reconnect and change their tools mid-session, and videos
|
|
11
|
+
made after reference videos.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Span events and links.** A run records what happened inside it as `RunEvent` values, each at its
|
|
16
|
+
time, and the runs it came from beside its parent as `RunLink` values. `Tracer.event()` records on
|
|
17
|
+
the run in scope, `RunHandle.event()` and `link()` on one run; `StartRunOptions.links` take a link
|
|
18
|
+
or a W3C `traceparent`, and `FinishRunOptions` take the `events` and `links` a run learns as it
|
|
19
|
+
ends. `maxEventsPerRun` (128) keeps a run's events bounded, counting the rest as `droppedEvents`.
|
|
20
|
+
Both reach OTLP as span events and links, in JSON, in protobuf, and over gRPC. The studio shows them
|
|
21
|
+
on a run, each link opening the run it names, and `#/traces/<trace>/<run>` opens one run of a trace.
|
|
22
|
+
- **Events where they happen.** A graph's retry is a `retry` event of its task's run, a tool's
|
|
23
|
+
progress a `progress` event of the tool's, and a task after a step of several, such as a fan-out's
|
|
24
|
+
join, links to each of them. A model run has the attempts that failed before its answer, as `retry`
|
|
25
|
+
and `failover` at the times they failed, and its guardrails, cache answer, and capability warnings.
|
|
26
|
+
- **Failed attempts on the response.** `ResponseMeta.failedAttempts` lists each `ResponseAttempt` that
|
|
27
|
+
failed before the answer: its provider, model, error, category, and time; a stream's last chunk
|
|
28
|
+
carries them too.
|
|
29
|
+
- **Media that knows its length.** `readMediaDuration()`, on `nexus-ai-pro/evaluate/media`, reads the
|
|
30
|
+
length of WAV, MP4 and MOV (fragmented MP4 included), WebM and Matroska, MP3, Ogg Vorbis and Opus,
|
|
31
|
+
and FLAC from their own headers, with no dependency. `mediaChecks()` holds audio and video to a
|
|
32
|
+
`durationMs` range with it, and a video model's `maxInputVideoSeconds` refuses a longer video before
|
|
33
|
+
it is sent: 10 seconds on Gemini Omni Flash, 30 on Veo.
|
|
34
|
+
- **Lists across regions.** The regional gateway answers `GET /threads` and `GET /runs` from every
|
|
35
|
+
region the tenant may reach, merged newest first and cut to the limit, naming the regions that
|
|
36
|
+
answered and, as `RegionListGap` values under `incomplete`, those that were down or failed. A
|
|
37
|
+
refusal every region gives is the answer. `lists: 'home'` keeps the home region's list alone.
|
|
38
|
+
- **Lists that page.** An agent server's `GET /threads`, `GET /runs`, and `GET /threads/:id/runs`
|
|
39
|
+
take a `cursor` and answer with a `nextCursor` while more follow; `RunManager.listThreads()` and
|
|
40
|
+
`listRuns()` return a `ListPage`. A cursor names a place in the order every region lists by, so the
|
|
41
|
+
regional gateway pages a merged list with it too, each record once, in order, however the records
|
|
42
|
+
fall between regions. A region that does not page by it contributes only what comes after it.
|
|
43
|
+
- **MCP that reconnects.** With `McpClientOptions.reconnect`, a client whose connection closed opens
|
|
44
|
+
it again at the next request and greets the server again: an SSE stream that ended, with
|
|
45
|
+
`SseClientOptions.reopen`, or a stdio server that exited, started again. A stream that cannot open,
|
|
46
|
+
or names no endpoint in time, is let go at once, so the next request tries again.
|
|
47
|
+
- **Tools that change.** `McpClient.onToolsChanged()` hears a server's
|
|
48
|
+
`notifications/tools/list_changed`. `McpServer.register()` sends one to a connected client, and the
|
|
49
|
+
server says it will. The editor bridge reconnects an editor's SSE servers within a session and lists
|
|
50
|
+
a server's tools again before the turn after it says they changed, keeping the last list when that
|
|
51
|
+
fails.
|
|
52
|
+
- **Reference videos.** `VideoGenerateRequest.referenceVideos` gives a model videos to take after.
|
|
53
|
+
Models declare `reference-videos` among their inputs, with `maxReferenceVideos` and
|
|
54
|
+
`referenceVideoSeconds`, each and together, read from a video's headers before it is sent. Gemini
|
|
55
|
+
Omni Flash takes three of three seconds each, as does Runway's `gemini_omni_flash_1.1`; Runway's
|
|
56
|
+
Seedance 2 models, `hailuo3`, and `wan3` take 15 seconds together, beside reference images. The
|
|
57
|
+
conformance suite checks them with `referenceVideo`.
|
|
58
|
+
- **Gemini Omni Flash on Vertex AI.** `GoogleVideoProvider` serves `gemini-omni-1.1-flash-preview` on
|
|
59
|
+
Vertex AI's Interactions API, at the global location for the project its `baseUrl` names, or at
|
|
60
|
+
`interactionsBaseUrl`.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- A list of threads or runs through the regional gateway comes from every region the tenant may
|
|
65
|
+
reach, not its home region alone; `RegionalGatewayOptions.lists: 'home'` keeps the 2.17 answer.
|
|
66
|
+
- An agent server lists threads and runs by when each was last written, then by id, the order its
|
|
67
|
+
cursor pages by. A `limit` that is not a whole number above 0 is refused with a 400.
|
|
68
|
+
- `McpServer` says its tools change, as `tools.listChanged`, and `register()` tells a connected
|
|
69
|
+
client.
|
|
70
|
+
- `VideoInputKind` has `reference-videos`; a `switch` over it that names every kind needs the new one.
|
|
71
|
+
- Tested on Node.js 26 as well as 22 and 24: CI runs the release gate on each, on the newest patch of
|
|
72
|
+
its line, so a Node.js security release, such as 2026-10-21's, is what the package is tested and
|
|
73
|
+
published on the day it ships.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- A tenant's threads and runs on a server several tenants share: a list read the newest records of
|
|
78
|
+
every tenant and then kept the tenant's, so another tenant's newer ones could crowd its page out,
|
|
79
|
+
down to an empty one. A list now reads until the tenant's page is full.
|
|
80
|
+
- The studio showed a run's metadata, its feedback, and a tool's progress as
|
|
81
|
+
`[object HTMLHeadingElement]` rather than their content.
|
|
82
|
+
|
|
7
83
|
## [2.17.0] - 2026-10-21
|
|
8
84
|
|
|
9
85
|
The editor's MCP servers over every transport, decided by what their servers say they do; and video
|
|
@@ -3103,7 +3179,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
3103
3179
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
3104
3180
|
- CLI and audit findings no longer disclose detected secret values.
|
|
3105
3181
|
|
|
3106
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
3182
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.18.0...HEAD
|
|
3183
|
+
[2.18.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.17.0...v2.18.0
|
|
3107
3184
|
[2.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...v2.17.0
|
|
3108
3185
|
[2.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...v2.16.0
|
|
3109
3186
|
[2.15.1]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.0...v2.15.1
|
package/README.md
CHANGED
|
@@ -327,12 +327,16 @@ Shipped so far:
|
|
|
327
327
|
as it is written;
|
|
328
328
|
- 2.17 the editor's MCP servers over every transport, and video you can extend or edit: an ACP agent
|
|
329
329
|
connects stdio, HTTP, and SSE servers and decides each tool by what its server says it does, and a
|
|
330
|
-
video is extended from its own job or edited on Gemini Omni Flash, Veo, and Runway
|
|
330
|
+
video is extended from its own job or edited on Gemini Omni Flash, Veo, and Runway;
|
|
331
|
+
- 2.18 span events and links, as OpenTelemetry has them, media that knows its length from its own
|
|
332
|
+
headers, a tenant's threads and runs listed and paged across all of its regions, MCP servers that
|
|
333
|
+
reconnect and change their tools within a session, and videos made after reference videos.
|
|
331
334
|
|
|
332
335
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
333
336
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
334
|
-
Next, 2.
|
|
335
|
-
|
|
337
|
+
Next, 2.19 reads a service's own API description into emulated tools, and lets an emulator hold
|
|
338
|
+
what a whole evaluation did, so examples that build on each other are evaluated with nothing sent.
|
|
339
|
+
Pinecone, images, the Gemini
|
|
336
340
|
embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
|
|
337
341
|
graduate once the live workflow passes against each. The
|
|
338
342
|
[roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
|
package/dist/evaluate/media.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type JudgeClient } from '../evals/judge.js';
|
|
7
7
|
import type { EvaluationContext, Evaluator, MediaPart } from '../types/evaluate.js';
|
|
8
|
+
export { type MediaDuration, readMediaDuration } from '../utils/media-duration.js';
|
|
8
9
|
/** What kind of medium something is. */
|
|
9
10
|
export type MediaSubjectKind = 'image' | 'audio' | 'video' | 'document' | 'other';
|
|
10
11
|
/** A medium read for judging: what it is, and as much of it as could be read. */
|
|
@@ -27,7 +28,10 @@ export interface ResolvedMedia {
|
|
|
27
28
|
width?: number;
|
|
28
29
|
/** Height in pixels. */
|
|
29
30
|
height?: number;
|
|
30
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Duration in milliseconds, read from the file's own headers: WAV, MP4 and MOV, WebM and Matroska,
|
|
33
|
+
* MP3, Ogg (Vorbis and Opus), and FLAC.
|
|
34
|
+
*/
|
|
31
35
|
durationMs?: number;
|
|
32
36
|
/** Why its bytes could not be read, when they could not. */
|
|
33
37
|
unreadable?: string;
|
|
@@ -93,7 +97,7 @@ export interface MediaCheckOptions extends MediaResolveOptions, MediaSelection {
|
|
|
93
97
|
ratio: number;
|
|
94
98
|
tolerance?: number;
|
|
95
99
|
};
|
|
96
|
-
/** The duration in milliseconds allowed, for audio whose
|
|
100
|
+
/** The duration in milliseconds allowed, for audio or video whose headers say it. */
|
|
97
101
|
durationMs?: MediaRange;
|
|
98
102
|
/** A medium whose measurement could not be read passes or fails that check. Defaults to `fail`. */
|
|
99
103
|
unknown?: 'pass' | 'fail';
|
package/dist/evaluate/media.js
CHANGED
|
@@ -4,6 +4,8 @@ import { decodeBase64, encodeBase64 } from '../utils/base64.js';
|
|
|
4
4
|
import { sha256Hex } from '../utils/sha256.js';
|
|
5
5
|
import { withProvenance } from './provenance.js';
|
|
6
6
|
import { asMediaPart, mediaParts } from './trace.js';
|
|
7
|
+
import { readMediaDuration } from '../utils/media-duration.js';
|
|
8
|
+
export { readMediaDuration } from '../utils/media-duration.js';
|
|
7
9
|
export function mediaOf(context, from = 'output') {
|
|
8
10
|
const traced = (direction) => (context.trace?.media ?? []).filter((item) => item.direction === direction).map((item) => item.part);
|
|
9
11
|
const output = () => {
|
|
@@ -65,11 +67,11 @@ export async function resolveMedia(part, options = {}) {
|
|
|
65
67
|
resolved.kind = 'document';
|
|
66
68
|
}
|
|
67
69
|
else {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
resolved.mimeType =
|
|
71
|
-
resolved.kind =
|
|
72
|
-
resolved.durationMs =
|
|
70
|
+
const read = readMediaDuration(bytes);
|
|
71
|
+
if (read) {
|
|
72
|
+
resolved.mimeType = read.mimeType;
|
|
73
|
+
resolved.kind = read.kind;
|
|
74
|
+
resolved.durationMs = read.durationMs;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -410,22 +412,6 @@ function mimeMatches(pattern, mimeType) {
|
|
|
410
412
|
return mimeType.startsWith(pattern.slice(0, -1));
|
|
411
413
|
return pattern === mimeType;
|
|
412
414
|
}
|
|
413
|
-
function wavDuration(bytes) {
|
|
414
|
-
if (!startsWith(bytes, 'RIFF') || ascii(bytes, 8, 4) !== 'WAVE')
|
|
415
|
-
return undefined;
|
|
416
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
417
|
-
let byteRate;
|
|
418
|
-
for (let at = 12; at + 8 <= bytes.length;) {
|
|
419
|
-
const id = ascii(bytes, at, 4);
|
|
420
|
-
const size = view.getUint32(at + 4, true);
|
|
421
|
-
if (id === 'fmt ' && at + 16 <= bytes.length)
|
|
422
|
-
byteRate = view.getUint32(at + 16, true);
|
|
423
|
-
if (id === 'data')
|
|
424
|
-
return byteRate ? Math.round((size / byteRate) * 1000) : undefined;
|
|
425
|
-
at += 8 + size + (size % 2);
|
|
426
|
-
}
|
|
427
|
-
return undefined;
|
|
428
|
-
}
|
|
429
415
|
function startsWith(bytes, text) {
|
|
430
416
|
return ascii(bytes, 0, text.length) === text;
|
|
431
417
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { tool, toolOutput } from './agent/tool.js';
|
|
|
20
20
|
export type { CacheHint, CompletionRequest, Message, MessageRole, ContentPart, PromptCacheConfig, ReasoningConfig, TextContent, ImageContent, AudioContent, BinaryBuffer, VideoContent, AssetContent, ToolChoice, ToolDefinition, ToolCallResult, ToolOutput, } from './types/messages.js';
|
|
21
21
|
export type { CapabilityConfig, CapabilityPolicy, CapabilityWarning, CapabilityWarningAction, } from './types/capabilities.js';
|
|
22
22
|
export type { CostEstimate, NexusPlan, } from './types/planning.js';
|
|
23
|
-
export type { NexusResponse, NexusStream, ResponseCost, ResponseMeta, StreamChunk, TokenUsage, ToolCall, } from './types/response.js';
|
|
23
|
+
export type { NexusResponse, NexusStream, ResponseAttempt, ResponseCost, ResponseMeta, StreamChunk, TokenUsage, ToolCall, } from './types/response.js';
|
|
24
24
|
export type { NexusAIConfig, ProvidersConfig, OpenAIProviderConfig, AnthropicProviderConfig, GoogleProviderConfig, OllamaProviderConfig, GroqProviderConfig, MistralProviderConfig, CohereProviderConfig, CustomProviderConfig, DeepSeekProviderConfig, AzureOpenAIProviderConfig, LMStudioProviderConfig, LlamaCppProviderConfig, RoutingConfig, RoutingRule, RoutingStrategy, FallbackConfig, ResponseFormatConfig, RetryConfig, CostBudgetConfig, AuditLogConfig, AuditLogEvent, LoggerConfig, LogEvent, LogLevel, RateLimitConfig, } from './types/config.js';
|
|
25
25
|
export type { ContextSummaryConfig, ContextSummaryInput, ContextSummaryMode, ContextSummarizer, ContextWindowConfig, ContextWindowResult, ContextWindowStrategy, ContextWindowUsage, } from './types/context-window.js';
|
|
26
26
|
export type { AliasMetadata, AliasStage, CacheTtl, InputModality, ModelCapabilities, ModelEndpoint, ModelStatus, OutputModality, PromptCachingCapability, ProviderCapabilities, ReasoningEffort, RoutingModelPreference, } from './types/providers.js';
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -91,6 +91,14 @@ export interface McpClientOptions {
|
|
|
91
91
|
};
|
|
92
92
|
/** How long a request waits before failing. Defaults to 30 seconds. */
|
|
93
93
|
timeoutMs?: number;
|
|
94
|
+
/**
|
|
95
|
+
* When the transport closes, as an SSE stream that ends or a server process that exits does, the
|
|
96
|
+
* next request opens it again and repeats the handshake, since the server's session went with it.
|
|
97
|
+
* Requests in flight still fail, and a client closed with `close()` stays closed. Needs a transport
|
|
98
|
+
* that can open again: `createStdioTransport` starts a new process, `createSseTransport` with
|
|
99
|
+
* `reopen` a new stream.
|
|
100
|
+
*/
|
|
101
|
+
reconnect?: boolean;
|
|
94
102
|
}
|
|
95
103
|
/**
|
|
96
104
|
* Talks to an MCP server.
|
|
@@ -109,7 +117,15 @@ export declare class McpClient {
|
|
|
109
117
|
private initialized;
|
|
110
118
|
private connecting;
|
|
111
119
|
private serverInfo;
|
|
120
|
+
/** Who hears that the server's tools changed. */
|
|
121
|
+
private readonly toolsChanged;
|
|
122
|
+
private stopped;
|
|
112
123
|
constructor(transport: McpTransport, options?: McpClientOptions);
|
|
124
|
+
/**
|
|
125
|
+
* Hears each `notifications/tools/list_changed` the server sends: its tools are no longer what
|
|
126
|
+
* `listTools()` returned. Returns a function that stops hearing.
|
|
127
|
+
*/
|
|
128
|
+
onToolsChanged(handler: () => void): () => void;
|
|
113
129
|
/**
|
|
114
130
|
* Performs the handshake and returns the server's name and version. Called automatically by the
|
|
115
131
|
* first request that needs it; requests made together share one handshake.
|
|
@@ -190,7 +206,8 @@ export declare function serverEnvironment(source: Record<string, string | undefi
|
|
|
190
206
|
* Runs an MCP server as a child process and speaks to it over its stdin and stdout.
|
|
191
207
|
*
|
|
192
208
|
* The transport most MCP servers ship with. `node:child_process` is imported lazily, so a browser
|
|
193
|
-
* or edge build that only uses the HTTP transport never reaches for it.
|
|
209
|
+
* or edge build that only uses the HTTP transport never reaches for it. When the process exits, the
|
|
210
|
+
* transport closes; started again, it runs a new one.
|
|
194
211
|
*/
|
|
195
212
|
export declare function createStdioTransport(options: StdioClientOptions): McpTransport;
|
|
196
213
|
/** Connects to a remote MCP server over HTTP. */
|
|
@@ -219,11 +236,17 @@ export interface SseClientOptions {
|
|
|
219
236
|
fetch?: typeof globalThis.fetch;
|
|
220
237
|
/** How long to wait for the server to name where messages go, in milliseconds. Defaults to 10 seconds. */
|
|
221
238
|
timeoutMs?: number;
|
|
239
|
+
/**
|
|
240
|
+
* Opens a new stream when the next message is sent after the stream ended, rather than staying
|
|
241
|
+
* closed. The server starts a new session on it, so an `McpClient` with `reconnect` greets it again.
|
|
242
|
+
*/
|
|
243
|
+
reopen?: boolean;
|
|
222
244
|
}
|
|
223
245
|
/**
|
|
224
246
|
* Speaks to an MCP server over HTTP with Server-Sent Events, as MCP's 2024-11-05 transport: one
|
|
225
247
|
* long `GET` the server answers on, and a `POST` per message to the endpoint the stream names
|
|
226
248
|
* first. The endpoint must be on the stream's own origin, so a server cannot send the messages, and
|
|
227
|
-
* their headers, anywhere else. When the stream ends, the transport closes
|
|
249
|
+
* their headers, anywhere else. When the stream ends, the transport closes; with `reopen`, the next
|
|
250
|
+
* message opens a new one.
|
|
228
251
|
*/
|
|
229
252
|
export declare function createSseTransport(options: SseClientOptions): McpTransport;
|
package/dist/mcp/client.js
CHANGED
|
@@ -15,11 +15,23 @@ export class McpClient {
|
|
|
15
15
|
initialized = false;
|
|
16
16
|
connecting;
|
|
17
17
|
serverInfo = {};
|
|
18
|
+
toolsChanged = new Set();
|
|
19
|
+
stopped = false;
|
|
18
20
|
constructor(transport, options = {}) {
|
|
19
21
|
this.transport = transport;
|
|
20
22
|
this.options = options;
|
|
21
23
|
transport.onMessage((message) => this.receive(message));
|
|
22
|
-
transport.onClose?.(() =>
|
|
24
|
+
transport.onClose?.(() => {
|
|
25
|
+
this.failAll(new McpError('The MCP connection closed'));
|
|
26
|
+
if (this.options.reconnect && !this.stopped)
|
|
27
|
+
this.initialized = false;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
onToolsChanged(handler) {
|
|
31
|
+
this.toolsChanged.add(handler);
|
|
32
|
+
return () => {
|
|
33
|
+
this.toolsChanged.delete(handler);
|
|
34
|
+
};
|
|
23
35
|
}
|
|
24
36
|
async connect() {
|
|
25
37
|
if (this.initialized)
|
|
@@ -116,6 +128,7 @@ export class McpClient {
|
|
|
116
128
|
});
|
|
117
129
|
}
|
|
118
130
|
async close() {
|
|
131
|
+
this.stopped = true;
|
|
119
132
|
this.failAll(new McpError('The MCP client was closed'));
|
|
120
133
|
await this.transport.close();
|
|
121
134
|
}
|
|
@@ -141,6 +154,16 @@ export class McpClient {
|
|
|
141
154
|
this.settle(message.id, message.result, message.error ? new McpError(message.error.message, message.error.code, message.error.data) : undefined);
|
|
142
155
|
return;
|
|
143
156
|
}
|
|
157
|
+
if (isNotification(message) && message.method === 'notifications/tools/list_changed') {
|
|
158
|
+
for (const handler of this.toolsChanged) {
|
|
159
|
+
try {
|
|
160
|
+
handler();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
144
167
|
if (isNotification(message) && message.method === 'notifications/progress') {
|
|
145
168
|
const params = (message.params ?? {});
|
|
146
169
|
const report = this.progress.get(String(params.progressToken));
|
|
@@ -235,11 +258,11 @@ export function serverEnvironment(source, options = {}) {
|
|
|
235
258
|
}
|
|
236
259
|
export function createStdioTransport(options) {
|
|
237
260
|
let child;
|
|
238
|
-
const decoder = new LineDecoder();
|
|
239
261
|
let onMessage = () => undefined;
|
|
240
262
|
let onClose = () => undefined;
|
|
241
263
|
return {
|
|
242
264
|
async start() {
|
|
265
|
+
const decoder = new LineDecoder();
|
|
243
266
|
const { spawn } = await import('node:child_process');
|
|
244
267
|
const spawned = spawn(options.command, options.args ?? [], {
|
|
245
268
|
cwd: options.cwd,
|
|
@@ -323,62 +346,73 @@ export function createSseTransport(options) {
|
|
|
323
346
|
let endpoint;
|
|
324
347
|
let opening;
|
|
325
348
|
let closed = false;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
349
|
+
let stopped = false;
|
|
350
|
+
let stream = new AbortController();
|
|
351
|
+
const ended = (from) => {
|
|
352
|
+
if (closed || from !== stream)
|
|
329
353
|
return;
|
|
330
354
|
closed = true;
|
|
331
355
|
for (const handler of onClose)
|
|
332
356
|
handler();
|
|
333
357
|
};
|
|
334
358
|
const open = async () => {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
359
|
+
const own = new AbortController();
|
|
360
|
+
stream = own;
|
|
361
|
+
try {
|
|
362
|
+
const response = await fetchImplementation(options.url, {
|
|
363
|
+
headers: { accept: 'text/event-stream', ...options.headers },
|
|
364
|
+
signal: own.signal,
|
|
365
|
+
});
|
|
366
|
+
if (!response.ok || !response.body)
|
|
367
|
+
throw new McpError(`MCP SSE transport failed with ${response.status}`);
|
|
368
|
+
const reader = response.body.getReader();
|
|
369
|
+
await new Promise((resolve, reject) => {
|
|
370
|
+
const timer = setTimeout(() => reject(new McpError(`The MCP server named no endpoint within ${options.timeoutMs ?? 10_000}ms`)), options.timeoutMs ?? 10_000);
|
|
371
|
+
const event = (name, data) => {
|
|
372
|
+
if (name === 'endpoint') {
|
|
373
|
+
if (endpoint)
|
|
374
|
+
return;
|
|
375
|
+
const target = new URL(data.trim(), options.url);
|
|
376
|
+
if (target.origin !== new URL(options.url).origin) {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
reject(new McpError(`The MCP server named an endpoint on another origin, ${target.origin}`));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
endpoint = target.href;
|
|
353
382
|
clearTimeout(timer);
|
|
354
|
-
|
|
355
|
-
stream.abort();
|
|
356
|
-
return;
|
|
383
|
+
resolve();
|
|
357
384
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
onMessage(JSON.parse(data));
|
|
385
|
+
else if (name === 'message') {
|
|
386
|
+
try {
|
|
387
|
+
onMessage(JSON.parse(data));
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
}
|
|
365
391
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
reject(error instanceof McpError ? error : new McpError(`The MCP event stream failed: ${String(error)}`));
|
|
377
|
-
ended();
|
|
392
|
+
};
|
|
393
|
+
void readEvents(reader, event).then(() => {
|
|
394
|
+
clearTimeout(timer);
|
|
395
|
+
reject(new McpError('The MCP event stream ended'));
|
|
396
|
+
ended(own);
|
|
397
|
+
}, (error) => {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
reject(error instanceof McpError ? error : new McpError(`The MCP event stream failed: ${String(error)}`));
|
|
400
|
+
ended(own);
|
|
401
|
+
});
|
|
378
402
|
});
|
|
379
|
-
}
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
own.abort();
|
|
406
|
+
ended(own);
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
380
409
|
};
|
|
381
410
|
const started = () => {
|
|
411
|
+
if (closed && options.reopen && !stopped) {
|
|
412
|
+
closed = false;
|
|
413
|
+
endpoint = undefined;
|
|
414
|
+
opening = undefined;
|
|
415
|
+
}
|
|
382
416
|
opening ??= open();
|
|
383
417
|
return opening;
|
|
384
418
|
};
|
|
@@ -404,8 +438,9 @@ export function createSseTransport(options) {
|
|
|
404
438
|
onClose.push(handler);
|
|
405
439
|
},
|
|
406
440
|
close() {
|
|
441
|
+
stopped = true;
|
|
407
442
|
stream.abort();
|
|
408
|
-
ended();
|
|
443
|
+
ended(stream);
|
|
409
444
|
},
|
|
410
445
|
};
|
|
411
446
|
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare class McpServer {
|
|
|
41
41
|
private readonly tools;
|
|
42
42
|
private transport;
|
|
43
43
|
constructor(options?: McpServerOptions);
|
|
44
|
-
/** Adds a tool. Returns the server, for chaining. */
|
|
44
|
+
/** Adds a tool, and tells a connected client its tools changed. Returns the server, for chaining. */
|
|
45
45
|
register(tool: ToolDefinition): this;
|
|
46
46
|
/** Starts serving requests over a transport. */
|
|
47
47
|
connect(transport: McpTransport): Promise<void>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -25,6 +25,9 @@ export class McpServer {
|
|
|
25
25
|
}
|
|
26
26
|
register(tool) {
|
|
27
27
|
this.tools.set(tool.name, tool);
|
|
28
|
+
if (this.transport) {
|
|
29
|
+
void Promise.resolve(this.transport.send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' })).catch(() => undefined);
|
|
30
|
+
}
|
|
28
31
|
return this;
|
|
29
32
|
}
|
|
30
33
|
async connect(transport) {
|
|
@@ -63,7 +66,7 @@ export class McpServer {
|
|
|
63
66
|
return {
|
|
64
67
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
65
68
|
capabilities: {
|
|
66
|
-
tools: { listChanged:
|
|
69
|
+
tools: { listChanged: true },
|
|
67
70
|
...(this.options.resources?.length ? { resources: { listChanged: false } } : {}),
|
|
68
71
|
},
|
|
69
72
|
serverInfo: { name: this.options.name ?? 'nexus-ai-pro', version: this.options.version ?? '1.x' },
|
|
@@ -7,8 +7,7 @@ export function acpEditor(options = {}) {
|
|
|
7
7
|
let connected = servers.get(session.id);
|
|
8
8
|
if (!connected) {
|
|
9
9
|
connected = (async () => {
|
|
10
|
-
const
|
|
11
|
-
const tools = [];
|
|
10
|
+
const entries = [];
|
|
12
11
|
const declare = options.mcpToolCapabilities ?? true;
|
|
13
12
|
for (const offered of session.mcpServers) {
|
|
14
13
|
const name = String(offered.name ?? 'mcp');
|
|
@@ -21,6 +20,7 @@ export function acpEditor(options = {}) {
|
|
|
21
20
|
...(options.mcpFetch ? { fetch: options.mcpFetch } : {}),
|
|
22
21
|
}
|
|
23
22
|
: undefined;
|
|
23
|
+
const sse = remote && offered.type === 'sse';
|
|
24
24
|
const transport = offered.command
|
|
25
25
|
? createStdioTransport({
|
|
26
26
|
command: offered.command,
|
|
@@ -28,26 +28,40 @@ export function acpEditor(options = {}) {
|
|
|
28
28
|
env: Object.fromEntries((offered.env ?? []).map((item) => [item.name, item.value])),
|
|
29
29
|
...(session.cwd ? { cwd: session.cwd } : {}),
|
|
30
30
|
})
|
|
31
|
-
:
|
|
32
|
-
?
|
|
31
|
+
: sse
|
|
32
|
+
?
|
|
33
|
+
createSseTransport({ ...remote, timeoutMs: options.mcpTimeoutMs ?? 30_000, reopen: true })
|
|
33
34
|
: remote && (offered.type === 'http' || offered.type === undefined)
|
|
34
35
|
? createHttpTransport(remote)
|
|
35
36
|
: undefined;
|
|
36
37
|
if (!transport)
|
|
37
38
|
throw new Error(`MCP server "${name}" uses a transport this agent does not speak`);
|
|
38
|
-
const client = new McpClient(transport, {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const client = new McpClient(transport, {
|
|
40
|
+
timeoutMs: options.mcpTimeoutMs ?? 30_000,
|
|
41
|
+
...(sse ? { reconnect: true } : {}),
|
|
42
|
+
});
|
|
43
|
+
const entry = {
|
|
44
|
+
name,
|
|
45
|
+
client,
|
|
46
|
+
tools: [],
|
|
47
|
+
stale: false,
|
|
48
|
+
list: () => client.toNexusTools({
|
|
49
|
+
prefix,
|
|
50
|
+
server: prefix,
|
|
51
|
+
capabilities: typeof declare === 'function' ? (tool) => declare(tool, prefix) : declare,
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
entries.push(entry);
|
|
55
|
+
client.onToolsChanged(() => {
|
|
56
|
+
entry.stale = true;
|
|
57
|
+
});
|
|
58
|
+
entry.tools = await entry.list();
|
|
45
59
|
}
|
|
46
60
|
catch (error) {
|
|
47
61
|
options.onError?.(error, name);
|
|
48
62
|
}
|
|
49
63
|
}
|
|
50
|
-
return
|
|
64
|
+
return entries;
|
|
51
65
|
})();
|
|
52
66
|
servers.set(session.id, connected);
|
|
53
67
|
}
|
|
@@ -155,13 +169,26 @@ export function acpEditor(options = {}) {
|
|
|
155
169
|
const own = tools(session);
|
|
156
170
|
if (options.mcp === false || session.mcpServers.length === 0)
|
|
157
171
|
return own;
|
|
158
|
-
|
|
172
|
+
const entries = await mcpTools(session);
|
|
173
|
+
await Promise.all(entries
|
|
174
|
+
.filter((entry) => entry.stale)
|
|
175
|
+
.map(async (entry) => {
|
|
176
|
+
entry.stale = false;
|
|
177
|
+
try {
|
|
178
|
+
entry.tools = await entry.list();
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
entry.stale = true;
|
|
182
|
+
options.onError?.(error, entry.name);
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
return [...own, ...entries.flatMap((entry) => entry.tools)];
|
|
159
186
|
},
|
|
160
187
|
async close() {
|
|
161
188
|
const connected = [...servers.values()];
|
|
162
189
|
servers.clear();
|
|
163
|
-
for (const
|
|
164
|
-
await Promise.all(
|
|
190
|
+
for (const entries of await Promise.all(connected)) {
|
|
191
|
+
await Promise.all(entries.map((entry) => entry.client.close().catch(() => undefined)));
|
|
165
192
|
}
|
|
166
193
|
},
|
|
167
194
|
};
|