nexus-ai-pro 2.15.1 → 2.16.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 +15 -0
- package/CHANGELOG.md +32 -1
- package/README.md +7 -3
- package/dist/protocols/a2a-grpc.d.ts +63 -0
- package/dist/protocols/a2a-grpc.js +374 -0
- package/dist/protocols/a2a-proto.d.ts +26 -0
- package/dist/protocols/a2a-proto.js +592 -0
- package/dist/protocols/a2a.d.ts +92 -7
- package/dist/protocols/a2a.js +451 -110
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/protocols/a2a-grpc.d.ts +63 -0
- package/dist-cjs/protocols/a2a-grpc.js +381 -0
- package/dist-cjs/protocols/a2a-proto.d.ts +26 -0
- package/dist-cjs/protocols/a2a-proto.js +599 -0
- package/dist-cjs/protocols/a2a.d.ts +92 -7
- package/dist-cjs/protocols/a2a.js +453 -110
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +14 -1
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,21 @@ 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.16 stage (2.16.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.16 adds is stable from the start and additive, unless this section says otherwise.
|
|
47
|
+
|
|
48
|
+
New and stable:
|
|
49
|
+
- `nexus-ai-pro/protocols/a2a/grpc`: `a2aGrpcService()`, `a2aGrpcClient()`, `A2aGrpcService`,
|
|
50
|
+
`A2aGrpcServiceOptions`, `A2aGrpcClient`, and `A2aGrpcClientOptions`;
|
|
51
|
+
- in `nexus-ai-pro/protocols/a2a`: the HTTP+JSON routes, `A2aServerOptions.bindings` and
|
|
52
|
+
`interfaces`, `A2aHandler.call()`, `card`, and `authenticate()`, `A2aBinding`, `A2aCallContext`,
|
|
53
|
+
`A2aCallOutcome`, `A2aErrorStatus`, `a2aErrorStatus()`, `a2aErrorCode()`,
|
|
54
|
+
`A2aClientOptions.binding`, `A2aPushOptions.text`, and `A2aPushNotificationConfig.authentication`.
|
|
55
|
+
|
|
56
|
+
The routes, the gRPC service, and their errors follow A2A 1.0's bindings: a change the specification
|
|
57
|
+
makes in a later version comes with that version, behind `A2A_PROTOCOL_VERSION`.
|
|
58
|
+
|
|
44
59
|
## 2.15 stage (2.15.0)
|
|
45
60
|
|
|
46
61
|
Everything 2.15 adds is stable from the start and additive, unless this section says otherwise.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.16.0] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
A2A on all three of its bindings over one set of tasks, and webhooks told the answer as it is written.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **A2A over HTTP+JSON and gRPC.** `a2aHandler()` serves the protocol's HTTP+JSON binding beside
|
|
14
|
+
JSON-RPC, at routes under the card's URL, its errors AIP-193's JSON with A2A's `ErrorInfo` reason.
|
|
15
|
+
`a2aGrpcService()`, on the new `nexus-ai-pro/protocols/a2a/grpc`, serves the gRPC binding,
|
|
16
|
+
`lf.a2a.v1.A2AService`, over `node:http2` with no dependency. The card names each binding served
|
|
17
|
+
(`bindings`, `interfaces`). All three run through `A2aHandler.call()` over one task store, so a
|
|
18
|
+
caller on any binding continues, reads, cancels, or follows a task another started, and reads the
|
|
19
|
+
same history.
|
|
20
|
+
- **Clients on every binding.** `a2aClient({ binding: 'HTTP+JSON' })` calls the HTTP+JSON binding, and
|
|
21
|
+
`a2aGrpcClient()` the gRPC one, with every method `a2aClient()` has. An error on any binding raises
|
|
22
|
+
an `A2aError` with A2A's code; `a2aErrorStatus()` and `a2aErrorCode()` map a code to its HTTP
|
|
23
|
+
status, gRPC status, and reason, and back.
|
|
24
|
+
- **A webhook told the answer as it is written.** `pushNotifications.text` pushes each piece of
|
|
25
|
+
streamed text to a task's webhooks, the same pieces a stream sends, before the state that follows
|
|
26
|
+
them; `text: { everyMs }` gathers the pieces into fewer deliveries. A push config's
|
|
27
|
+
`authentication` is sent as each delivery's `Authorization` header.
|
|
28
|
+
- Push configs take protocol 1.0's flat shape on every binding, and JSON-RPC answers both it and the
|
|
29
|
+
drafts' nested one; `ListTaskPushNotificationConfigs` is answered under its 1.0 name too.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The nightly interoperability check drives `@a2a-js/sdk` over each of the three bindings, and a
|
|
34
|
+
task across them. It drives `@agentclientprotocol/sdk` 2.0, which dropped
|
|
35
|
+
`ClientSideConnection`, through its new connection API, and 1.x as before.
|
|
36
|
+
|
|
7
37
|
## [2.15.1] - 2026-10-19
|
|
8
38
|
|
|
9
39
|
A test that could fail by chance, made exact. The package is unchanged from 2.15.0.
|
|
@@ -3021,7 +3051,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
3021
3051
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
3022
3052
|
- CLI and audit findings no longer disclose detected secret values.
|
|
3023
3053
|
|
|
3024
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
3054
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...HEAD
|
|
3055
|
+
[2.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...v2.16.0
|
|
3025
3056
|
[2.15.1]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.0...v2.15.1
|
|
3026
3057
|
[2.15.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.14.0...v2.15.0
|
|
3027
3058
|
[2.14.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.13.0...v2.14.0
|
package/README.md
CHANGED
|
@@ -321,12 +321,16 @@ Shipped so far:
|
|
|
321
321
|
- 2.15 a compatibility record for community adapters: `nexus adapters verify` seals the contract an
|
|
322
322
|
adapter passed, against which release, with its benchmark; `nexus adapters recheck` turns a listed
|
|
323
323
|
adapter red before a release that would break it; and one
|
|
324
|
-
[page](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/COMPATIBILITY.md) lists them
|
|
324
|
+
[page](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/COMPATIBILITY.md) lists them;
|
|
325
|
+
- 2.16 A2A over every binding: one agent served over JSON-RPC, HTTP+JSON, and gRPC from one task
|
|
326
|
+
store, so a caller on any binding continues a task another started, and webhooks told the answer
|
|
327
|
+
as it is written.
|
|
325
328
|
|
|
326
329
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
327
330
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
328
|
-
Next, 2.
|
|
329
|
-
|
|
331
|
+
Next, 2.17 connects the MCP servers an editor gives an ACP agent over SSE as well as stdio and HTTP,
|
|
332
|
+
and reads what their tools declare they do, so a permission policy covers them without a rule for
|
|
333
|
+
each. Pinecone, images, the Gemini
|
|
330
334
|
embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
|
|
331
335
|
graduate once the live workflow passes against each. The
|
|
332
336
|
[roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists what comes after, in the order
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A's gRPC binding: the service `lf.a2a.v1.A2AService` of protocol 1.0, served over the same
|
|
3
|
+
* `a2aHandler()` as its JSON-RPC and HTTP+JSON bindings, and so over the same tasks, and a client that
|
|
4
|
+
* calls an agent over it. Over HTTP/2 from `node:http2`, with no dependency.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const handler = a2aHandler(agent, { card, interfaces: [{ url: 'agents.example.com:50051', protocolBinding: 'GRPC' }] });
|
|
8
|
+
* const grpc = a2aGrpcService(handler);
|
|
9
|
+
* await grpc.listen(50051);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { type Http2Server, type IncomingHttpHeaders, type ServerHttp2Stream } from 'node:http2';
|
|
13
|
+
import { type A2aClient, type A2aClientOptions, type A2aHandler } from './a2a.js';
|
|
14
|
+
/** Options for `a2aGrpcService()`. */
|
|
15
|
+
export interface A2aGrpcServiceOptions {
|
|
16
|
+
/** Largest request message taken, in bytes. Defaults to 4 MiB, gRPC's own default. */
|
|
17
|
+
maxMessageBytes?: number;
|
|
18
|
+
}
|
|
19
|
+
/** The gRPC binding of one A2A handler. */
|
|
20
|
+
export interface A2aGrpcService {
|
|
21
|
+
/**
|
|
22
|
+
* Answers one HTTP/2 stream: give it each stream of a server of your own, such as one over TLS,
|
|
23
|
+
* as `server.on('stream', service.handle)`.
|
|
24
|
+
*/
|
|
25
|
+
handle(stream: ServerHttp2Stream, headers: IncomingHttpHeaders): void;
|
|
26
|
+
/**
|
|
27
|
+
* Starts a server over plain HTTP/2 on `port`, 0 for any, on `host`: for a server behind a proxy
|
|
28
|
+
* that ends TLS, and for tests. Resolves with the address to list on the card, `host:port`. Its
|
|
29
|
+
* `close()` stops taking calls, tells each connected client to go, and resolves once the calls in
|
|
30
|
+
* flight finish.
|
|
31
|
+
*/
|
|
32
|
+
listen(port?: number, host?: string): Promise<{
|
|
33
|
+
url: string;
|
|
34
|
+
port: number;
|
|
35
|
+
server: Http2Server;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The gRPC binding over an `a2aHandler()`: each method of `lf.a2a.v1.A2AService`, its message read as
|
|
41
|
+
* the JSON the handler takes and its answer written back, a stream as a server-streaming call, and an
|
|
42
|
+
* error as a gRPC status with A2A's `ErrorInfo` in `grpc-status-details-bin`. The caller is read by the
|
|
43
|
+
* handler's own `principal`, from a `Request` with the call's metadata as its headers.
|
|
44
|
+
*/
|
|
45
|
+
export declare function a2aGrpcService(handler: A2aHandler, options?: A2aGrpcServiceOptions): A2aGrpcService;
|
|
46
|
+
/** Options for `a2aGrpcClient()`. */
|
|
47
|
+
export interface A2aGrpcClientOptions extends Omit<A2aClientOptions, 'binding' | 'url'> {
|
|
48
|
+
/** Where the agent's gRPC service is: `host:port`, as its card lists it, or an `http://` or `https://` URL. */
|
|
49
|
+
url: string;
|
|
50
|
+
/** Where its card is, for `card()`. Without it, `card()` reads the extended card. */
|
|
51
|
+
cardUrl?: string;
|
|
52
|
+
}
|
|
53
|
+
/** A client for an A2A agent over gRPC, with `close()` for its connection. */
|
|
54
|
+
export interface A2aGrpcClient extends A2aClient {
|
|
55
|
+
/** Closes the HTTP/2 connection. */
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A client for one A2A agent over its gRPC binding: the same methods as `a2aClient()`, each a call of
|
|
60
|
+
* `lf.a2a.v1.A2AService` over one HTTP/2 connection, a gRPC error raised as an `A2aError` with the
|
|
61
|
+
* protocol's code. Plain HTTP/2 for `host:port` and `http://`; TLS for `https://`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function a2aGrpcClient(options: A2aGrpcClientOptions): A2aGrpcClient;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import http2 from 'node:http2';
|
|
2
|
+
import { gunzipSync, inflateSync } from 'node:zlib';
|
|
3
|
+
import { A2A_PROTOCOL_VERSION, A2aError, a2aErrorCode, a2aErrorStatus, } from './a2a.js';
|
|
4
|
+
import { A2A_GRPC_METHODS, A2A_GRPC_SERVICE, a2aStatusReason, decodeA2a, encodeA2a, encodeA2aStatus, } from './a2a-proto.js';
|
|
5
|
+
import { protocolId } from './shared.js';
|
|
6
|
+
const GRPC_CODES = {
|
|
7
|
+
OK: 0,
|
|
8
|
+
CANCELLED: 1,
|
|
9
|
+
UNKNOWN: 2,
|
|
10
|
+
INVALID_ARGUMENT: 3,
|
|
11
|
+
DEADLINE_EXCEEDED: 4,
|
|
12
|
+
NOT_FOUND: 5,
|
|
13
|
+
ALREADY_EXISTS: 6,
|
|
14
|
+
PERMISSION_DENIED: 7,
|
|
15
|
+
RESOURCE_EXHAUSTED: 8,
|
|
16
|
+
FAILED_PRECONDITION: 9,
|
|
17
|
+
ABORTED: 10,
|
|
18
|
+
OUT_OF_RANGE: 11,
|
|
19
|
+
UNIMPLEMENTED: 12,
|
|
20
|
+
INTERNAL: 13,
|
|
21
|
+
UNAVAILABLE: 14,
|
|
22
|
+
DATA_LOSS: 15,
|
|
23
|
+
UNAUTHENTICATED: 16,
|
|
24
|
+
};
|
|
25
|
+
export function a2aGrpcService(handler, options = {}) {
|
|
26
|
+
const maxBytes = options.maxMessageBytes ?? 4 * 1024 * 1024;
|
|
27
|
+
const handle = (stream, headers) => {
|
|
28
|
+
const path = String(headers[':path'] ?? '');
|
|
29
|
+
const method = path.startsWith(`/${A2A_GRPC_SERVICE}/`) ? path.slice(A2A_GRPC_SERVICE.length + 2) : '';
|
|
30
|
+
const shape = A2A_GRPC_METHODS[method];
|
|
31
|
+
const contentType = String(headers['content-type'] ?? '');
|
|
32
|
+
if (!contentType.startsWith('application/grpc')) {
|
|
33
|
+
stream.respond({ ':status': 415 }, { endStream: true });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!shape) {
|
|
37
|
+
fail(stream, GRPC_CODES.UNIMPLEMENTED, `Method ${path} is not part of ${A2A_GRPC_SERVICE}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const version = headers['a2a-version'];
|
|
41
|
+
if (typeof version === 'string' && !version.startsWith('1.')) {
|
|
42
|
+
failA2a(stream, -32009, `This agent speaks A2A ${A2A_PROTOCOL_VERSION}, not ${version}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = deadline(headers['grpc-timeout']);
|
|
47
|
+
const timer = timeout === undefined ? undefined : setTimeout(() => controller.abort(new Error('deadline exceeded')), timeout);
|
|
48
|
+
stream.on('close', () => {
|
|
49
|
+
if (timer)
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
controller.abort(new Error('The caller went away'));
|
|
52
|
+
});
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let size = 0;
|
|
55
|
+
stream.on('data', (chunk) => {
|
|
56
|
+
size += chunk.length;
|
|
57
|
+
if (size <= maxBytes + 5)
|
|
58
|
+
chunks.push(chunk);
|
|
59
|
+
});
|
|
60
|
+
stream.on('end', () => {
|
|
61
|
+
void (async () => {
|
|
62
|
+
if (size > maxBytes + 5) {
|
|
63
|
+
return fail(stream, GRPC_CODES.RESOURCE_EXHAUSTED, `The request is larger than ${maxBytes} bytes`);
|
|
64
|
+
}
|
|
65
|
+
let params;
|
|
66
|
+
try {
|
|
67
|
+
const [message] = frames(Buffer.concat(chunks), headers['grpc-encoding']);
|
|
68
|
+
params = decodeA2a(shape.request, message ?? new Uint8Array());
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return fail(stream, GRPC_CODES.INVALID_ARGUMENT, error.message);
|
|
72
|
+
}
|
|
73
|
+
const request = new Request(`http://grpc.invalid${path}`, { method: 'POST', headers: metadataOf(headers) });
|
|
74
|
+
const principal = await handler.authenticate(request);
|
|
75
|
+
const outcome = await handler.call(method, params, {
|
|
76
|
+
...(principal ? { principal } : {}),
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
binding: 'GRPC',
|
|
79
|
+
});
|
|
80
|
+
if ('error' in outcome)
|
|
81
|
+
return failA2a(stream, outcome.error.code, outcome.error.message);
|
|
82
|
+
if (stream.destroyed)
|
|
83
|
+
return;
|
|
84
|
+
stream.respond({ ':status': 200, 'content-type': 'application/grpc+proto' }, { waitForTrailers: true });
|
|
85
|
+
let trailers = { 'grpc-status': '0' };
|
|
86
|
+
stream.on('wantTrailers', () => stream.sendTrailers(trailers));
|
|
87
|
+
if ('events' in outcome) {
|
|
88
|
+
const reader = outcome.events.getReader();
|
|
89
|
+
try {
|
|
90
|
+
for (;;) {
|
|
91
|
+
const { value, done } = await reader.read();
|
|
92
|
+
if (done || stream.destroyed)
|
|
93
|
+
break;
|
|
94
|
+
stream.write(frame(encodeA2a(shape.response, value)));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
trailers = errorTrailers(-32603, error.message);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (stream.destroyed)
|
|
102
|
+
void reader.cancel();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
stream.write(frame(encodeA2a(shape.response, outcome.result ?? {})));
|
|
107
|
+
}
|
|
108
|
+
if (!stream.destroyed)
|
|
109
|
+
stream.end();
|
|
110
|
+
})().catch((error) => {
|
|
111
|
+
if (!stream.headersSent)
|
|
112
|
+
fail(stream, GRPC_CODES.INTERNAL, error.message);
|
|
113
|
+
else
|
|
114
|
+
stream.destroy(error);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
handle,
|
|
120
|
+
async listen(port = 0, host = '127.0.0.1') {
|
|
121
|
+
const server = http2.createServer();
|
|
122
|
+
server.on('stream', handle);
|
|
123
|
+
const sessions = new Set();
|
|
124
|
+
server.on('session', (session) => {
|
|
125
|
+
sessions.add(session);
|
|
126
|
+
session.on('close', () => sessions.delete(session));
|
|
127
|
+
});
|
|
128
|
+
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
129
|
+
const address = server.address();
|
|
130
|
+
return {
|
|
131
|
+
url: `${host}:${address.port}`,
|
|
132
|
+
port: address.port,
|
|
133
|
+
server,
|
|
134
|
+
close: () => new Promise((resolve) => {
|
|
135
|
+
server.close(() => resolve());
|
|
136
|
+
for (const session of sessions)
|
|
137
|
+
session.close();
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function fail(stream, code, message, details) {
|
|
144
|
+
if (stream.destroyed || stream.headersSent)
|
|
145
|
+
return;
|
|
146
|
+
stream.respond({
|
|
147
|
+
':status': 200,
|
|
148
|
+
'content-type': 'application/grpc+proto',
|
|
149
|
+
'grpc-status': String(code),
|
|
150
|
+
'grpc-message': grpcMessage(message),
|
|
151
|
+
...(details ? { 'grpc-status-details-bin': Buffer.from(details).toString('base64').replace(/=+$/, '') } : {}),
|
|
152
|
+
}, { endStream: true });
|
|
153
|
+
}
|
|
154
|
+
function failA2a(stream, code, message) {
|
|
155
|
+
const status = a2aErrorStatus(code);
|
|
156
|
+
const grpc = GRPC_CODES[status.grpcStatus] ?? GRPC_CODES.INTERNAL;
|
|
157
|
+
fail(stream, grpc, message, encodeA2aStatus(grpc, message, status.reason));
|
|
158
|
+
}
|
|
159
|
+
function errorTrailers(code, message) {
|
|
160
|
+
const status = a2aErrorStatus(code);
|
|
161
|
+
const grpc = GRPC_CODES[status.grpcStatus] ?? GRPC_CODES.INTERNAL;
|
|
162
|
+
return {
|
|
163
|
+
'grpc-status': String(grpc),
|
|
164
|
+
'grpc-message': grpcMessage(message),
|
|
165
|
+
'grpc-status-details-bin': Buffer.from(encodeA2aStatus(grpc, message, status.reason))
|
|
166
|
+
.toString('base64')
|
|
167
|
+
.replace(/=+$/, ''),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function frame(message) {
|
|
171
|
+
const out = new Uint8Array(5 + message.length);
|
|
172
|
+
new DataView(out.buffer).setUint32(1, message.length);
|
|
173
|
+
out.set(message, 5);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
function frames(body, encoding) {
|
|
177
|
+
const out = [];
|
|
178
|
+
let offset = 0;
|
|
179
|
+
while (offset + 5 <= body.length) {
|
|
180
|
+
const compressed = body[offset] === 1;
|
|
181
|
+
const length = body.readUInt32BE(offset + 1);
|
|
182
|
+
let message = body.subarray(offset + 5, offset + 5 + length);
|
|
183
|
+
if (message.length < length)
|
|
184
|
+
throw new Error('A gRPC message ended early');
|
|
185
|
+
if (compressed) {
|
|
186
|
+
const name = String(encoding ?? 'identity');
|
|
187
|
+
if (name === 'gzip')
|
|
188
|
+
message = gunzipSync(message);
|
|
189
|
+
else if (name === 'deflate')
|
|
190
|
+
message = inflateSync(message);
|
|
191
|
+
else
|
|
192
|
+
throw new Error(`A gRPC message compressed with ${name} cannot be read`);
|
|
193
|
+
}
|
|
194
|
+
out.push(message);
|
|
195
|
+
offset += 5 + length;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function deadline(value) {
|
|
200
|
+
const match = typeof value === 'string' ? /^(\d+)([HMSmun])$/.exec(value) : null;
|
|
201
|
+
if (!match)
|
|
202
|
+
return undefined;
|
|
203
|
+
const amount = Number(match[1]);
|
|
204
|
+
const unit = { H: 3_600_000, M: 60_000, S: 1_000, m: 1, u: 0.001, n: 0.000001 }[match[2]];
|
|
205
|
+
return Math.max(0, amount * unit);
|
|
206
|
+
}
|
|
207
|
+
function grpcMessage(message) {
|
|
208
|
+
let out = '';
|
|
209
|
+
for (const byte of new TextEncoder().encode(message)) {
|
|
210
|
+
out +=
|
|
211
|
+
byte >= 0x20 && byte <= 0x7e && byte !== 0x25
|
|
212
|
+
? String.fromCharCode(byte)
|
|
213
|
+
: `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
function decodeGrpcMessage(value) {
|
|
218
|
+
try {
|
|
219
|
+
return decodeURIComponent(value);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function metadataOf(headers) {
|
|
226
|
+
const out = new Headers();
|
|
227
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
228
|
+
if (name.startsWith(':') || value === undefined || name.endsWith('-bin'))
|
|
229
|
+
continue;
|
|
230
|
+
out.set(name, Array.isArray(value) ? value.join(', ') : String(value));
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
export function a2aGrpcClient(options) {
|
|
235
|
+
const authority = /^https?:\/\//.test(options.url) ? options.url : `http://${options.url}`;
|
|
236
|
+
let session;
|
|
237
|
+
let active = 0;
|
|
238
|
+
const connect = () => {
|
|
239
|
+
if (!session || session.closed || session.destroyed) {
|
|
240
|
+
session = http2.connect(authority);
|
|
241
|
+
session.on('error', () => undefined);
|
|
242
|
+
session.unref();
|
|
243
|
+
}
|
|
244
|
+
return session;
|
|
245
|
+
};
|
|
246
|
+
async function* invoke(method, params, signal) {
|
|
247
|
+
const shape = A2A_GRPC_METHODS[method];
|
|
248
|
+
const connection = connect();
|
|
249
|
+
active += 1;
|
|
250
|
+
connection.ref();
|
|
251
|
+
const request = connection.request({
|
|
252
|
+
':method': 'POST',
|
|
253
|
+
':path': `/${A2A_GRPC_SERVICE}/${method}`,
|
|
254
|
+
'content-type': 'application/grpc+proto',
|
|
255
|
+
te: 'trailers',
|
|
256
|
+
'a2a-version': A2A_PROTOCOL_VERSION,
|
|
257
|
+
...Object.fromEntries(Object.entries(options.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value])),
|
|
258
|
+
});
|
|
259
|
+
const abort = () => request.close(http2.constants.NGHTTP2_CANCEL);
|
|
260
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
261
|
+
request.end(frame(encodeA2a(shape.request, params)));
|
|
262
|
+
let status = {};
|
|
263
|
+
request.on('response', (headers) => {
|
|
264
|
+
if (headers['grpc-status'] !== undefined)
|
|
265
|
+
status = headers;
|
|
266
|
+
});
|
|
267
|
+
request.on('trailers', (trailers) => {
|
|
268
|
+
status = trailers;
|
|
269
|
+
});
|
|
270
|
+
let buffer = Buffer.alloc(0);
|
|
271
|
+
try {
|
|
272
|
+
for await (const chunk of request) {
|
|
273
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
274
|
+
while (buffer.length >= 5) {
|
|
275
|
+
const length = buffer.readUInt32BE(1);
|
|
276
|
+
if (buffer.length < 5 + length)
|
|
277
|
+
break;
|
|
278
|
+
yield decodeA2a(shape.response, buffer.subarray(5, 5 + length));
|
|
279
|
+
buffer = buffer.subarray(5 + length);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const code = Number(status['grpc-status'] ?? 2);
|
|
283
|
+
if (code !== 0) {
|
|
284
|
+
const details = status['grpc-status-details-bin'];
|
|
285
|
+
const info = typeof details === 'string' ? a2aStatusReason(Buffer.from(details, 'base64')) : {};
|
|
286
|
+
const message = info.message ?? decodeGrpcMessage(String(status['grpc-message'] ?? `gRPC status ${code}`));
|
|
287
|
+
throw new A2aError(a2aErrorCode(info.reason, code === 5 ? -32001 : -32603), message, { grpcStatus: code });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
signal?.removeEventListener('abort', abort);
|
|
292
|
+
active -= 1;
|
|
293
|
+
if (active === 0)
|
|
294
|
+
connection.unref();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const unary = async (method, params, signal) => {
|
|
298
|
+
let answer;
|
|
299
|
+
for await (const message of invoke(method, params, signal))
|
|
300
|
+
answer = message;
|
|
301
|
+
return (answer ?? {});
|
|
302
|
+
};
|
|
303
|
+
const message = (text, send) => ({
|
|
304
|
+
messageId: protocolId('message'),
|
|
305
|
+
role: 'ROLE_USER',
|
|
306
|
+
parts: [{ text }, ...(send.data === undefined ? [] : [{ data: send.data }])],
|
|
307
|
+
...(send.contextId ? { contextId: send.contextId } : {}),
|
|
308
|
+
...(send.taskId ? { taskId: send.taskId } : {}),
|
|
309
|
+
});
|
|
310
|
+
const configuration = (send) => ({
|
|
311
|
+
returnImmediately: send.returnImmediately === true,
|
|
312
|
+
...(send.push ? { taskPushNotificationConfig: send.push } : {}),
|
|
313
|
+
});
|
|
314
|
+
const pushConfig = (answer) => {
|
|
315
|
+
const { taskId: _taskId, tenant: _tenant, ...config } = answer;
|
|
316
|
+
return config;
|
|
317
|
+
};
|
|
318
|
+
const card = async () => {
|
|
319
|
+
if (!options.cardUrl)
|
|
320
|
+
return unary('GetExtendedAgentCard', {});
|
|
321
|
+
const response = await (options.fetch ?? globalThis.fetch)(options.cardUrl, {
|
|
322
|
+
headers: { accept: 'application/json', ...options.headers },
|
|
323
|
+
});
|
|
324
|
+
if (!response.ok)
|
|
325
|
+
throw new A2aError(response.status, `The agent card answered ${response.status}`);
|
|
326
|
+
return (await response.json());
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
card,
|
|
330
|
+
extendedCard: () => unary('GetExtendedAgentCard', {}),
|
|
331
|
+
async send(text, send = {}) {
|
|
332
|
+
const result = await unary('SendMessage', { message: message(text, send), configuration: configuration(send) }, send.signal);
|
|
333
|
+
if (result.task)
|
|
334
|
+
return result.task;
|
|
335
|
+
const reply = result.message;
|
|
336
|
+
return {
|
|
337
|
+
id: reply?.taskId ?? '',
|
|
338
|
+
contextId: reply?.contextId ?? send.contextId ?? '',
|
|
339
|
+
status: { state: 'TASK_STATE_COMPLETED', ...(reply ? { message: reply } : {}) },
|
|
340
|
+
artifacts: [{ artifactId: 'message', parts: reply?.parts ?? [] }],
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
async *stream(text, send = {}) {
|
|
344
|
+
for await (const event of invoke('SendStreamingMessage', { message: message(text, send), ...(send.push ? { configuration: configuration(send) } : {}) }, send.signal)) {
|
|
345
|
+
yield event;
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
getTask: (taskId, historyLength) => unary('GetTask', { id: taskId, ...(historyLength ? { historyLength } : {}) }),
|
|
349
|
+
listTasks: (list = {}) => unary('ListTasks', { ...list }),
|
|
350
|
+
async *subscribe(taskId, subscribe = {}) {
|
|
351
|
+
for await (const event of invoke('SubscribeToTask', { id: taskId }, subscribe.signal)) {
|
|
352
|
+
yield event;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
cancel: (taskId) => unary('CancelTask', { id: taskId }),
|
|
356
|
+
async setPushNotification(taskId, config) {
|
|
357
|
+
return pushConfig(await unary('CreateTaskPushNotificationConfig', { ...config, taskId }));
|
|
358
|
+
},
|
|
359
|
+
async listPushNotifications(taskId) {
|
|
360
|
+
const result = await unary('ListTaskPushNotificationConfigs', {
|
|
361
|
+
taskId,
|
|
362
|
+
});
|
|
363
|
+
return (result.configs ?? []).map(pushConfig);
|
|
364
|
+
},
|
|
365
|
+
async deletePushNotification(taskId, configId) {
|
|
366
|
+
await unary('DeleteTaskPushNotificationConfig', { taskId, id: configId });
|
|
367
|
+
},
|
|
368
|
+
close: () => new Promise((resolve) => {
|
|
369
|
+
if (!session || session.closed)
|
|
370
|
+
return resolve();
|
|
371
|
+
session.close(() => resolve());
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A's messages in Protocol Buffers, as `specification/a2a.proto` of protocol 1.0 numbers them
|
|
3
|
+
* (package `lf.a2a.v1`): each turned into, and read from, the JSON the other bindings carry, so the
|
|
4
|
+
* gRPC binding calls the same methods with the same params. With `google.protobuf.Struct`, `Value`,
|
|
5
|
+
* `Timestamp`, and `google.rpc.Status`. Fields the agent never sends, such as a card's security
|
|
6
|
+
* schemes, are read past and not written.
|
|
7
|
+
*/
|
|
8
|
+
/** Each method of `lf.a2a.v1.A2AService`: what it takes, what it answers, and whether it streams. */
|
|
9
|
+
export declare const A2A_GRPC_METHODS: Readonly<Record<string, {
|
|
10
|
+
request: string;
|
|
11
|
+
response: string;
|
|
12
|
+
stream?: boolean;
|
|
13
|
+
}>>;
|
|
14
|
+
/** The service's path prefix: `/lf.a2a.v1.A2AService/<method>`. */
|
|
15
|
+
export declare const A2A_GRPC_SERVICE = "lf.a2a.v1.A2AService";
|
|
16
|
+
/** Writes the JSON of a message as its Protocol Buffers bytes. */
|
|
17
|
+
export declare function encodeA2a(type: string, value: unknown): Uint8Array;
|
|
18
|
+
/** Reads a message's bytes as its JSON. */
|
|
19
|
+
export declare function decodeA2a(type: string, bytes: Uint8Array): Record<string, unknown>;
|
|
20
|
+
/** A `google.rpc.Status` with A2A's `ErrorInfo`, as `grpc-status-details-bin` carries it. */
|
|
21
|
+
export declare function encodeA2aStatus(code: number, message: string, reason: string): Uint8Array;
|
|
22
|
+
/** The `ErrorInfo` reason of a `google.rpc.Status`, when it has one. */
|
|
23
|
+
export declare function a2aStatusReason(bytes: Uint8Array): {
|
|
24
|
+
message?: string;
|
|
25
|
+
reason?: string;
|
|
26
|
+
};
|