@cleocode/lafs-protocol 1.2.3 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -5
- package/dist/examples/discovery-server.js +4 -4
- package/dist/src/a2a/bindings/grpc.d.ts +67 -0
- package/dist/src/a2a/bindings/grpc.js +148 -0
- package/dist/src/a2a/bindings/http.d.ts +92 -0
- package/dist/src/a2a/bindings/http.js +99 -0
- package/dist/src/a2a/bindings/index.d.ts +31 -0
- package/dist/src/a2a/bindings/index.js +54 -0
- package/dist/src/a2a/bindings/jsonrpc.d.ts +77 -0
- package/dist/src/a2a/bindings/jsonrpc.js +114 -0
- package/dist/src/a2a/bridge.d.ts +121 -76
- package/dist/src/a2a/bridge.js +185 -89
- package/dist/src/a2a/extensions.d.ts +93 -0
- package/dist/src/a2a/extensions.js +147 -0
- package/dist/src/a2a/index.d.ts +27 -25
- package/dist/src/a2a/index.js +59 -25
- package/dist/src/a2a/task-lifecycle.d.ts +98 -0
- package/dist/src/a2a/task-lifecycle.js +263 -0
- package/dist/src/discovery.d.ts +203 -44
- package/dist/src/discovery.js +211 -165
- package/dist/src/health/index.js +10 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +10 -1
- package/dist/src/types.d.ts +2 -1
- package/lafs.md +1 -1
- package/package.json +14 -1
- package/schemas/v1/agent-card.schema.json +230 -0
package/dist/src/a2a/bridge.js
CHANGED
|
@@ -1,79 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LAFS A2A Bridge
|
|
2
|
+
* LAFS A2A Bridge v2.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Full integration with official @a2a-js/sdk for Agent-to-Agent communication.
|
|
5
|
+
* Implements A2A Protocol v1.0+ specification.
|
|
6
|
+
*
|
|
7
|
+
* Reference: specs/external/specification.md
|
|
6
8
|
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Result Wrapper
|
|
11
|
+
// ============================================================================
|
|
7
12
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
13
|
-
* import { withLafsEnvelope } from '@lafs/envelope/a2a';
|
|
14
|
-
*
|
|
15
|
-
* const factory = new ClientFactory();
|
|
16
|
-
* const a2aClient = await factory.createFromUrl('http://localhost:4000');
|
|
17
|
-
*
|
|
18
|
-
* const client = withLafsEnvelope(a2aClient, {
|
|
19
|
-
* envelopeResponses: true,
|
|
20
|
-
* defaultBudget: { maxTokens: 4000 }
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* const result = await client.sendMessage({
|
|
24
|
-
* message: { role: 'user', parts: [{ text: 'Hello' }] }
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* // Access LAFS envelope
|
|
28
|
-
* const envelope = result.getLafsEnvelope();
|
|
29
|
-
* console.log(envelope._meta._tokenEstimate);
|
|
30
|
-
* ```
|
|
13
|
+
* Wrapper for A2A responses with LAFS envelope support
|
|
31
14
|
*/
|
|
32
|
-
export function withLafsEnvelope(client, config = {}) {
|
|
33
|
-
return new LafsA2AClient(client, config);
|
|
34
|
-
}
|
|
35
|
-
export class LafsA2AClient {
|
|
36
|
-
client;
|
|
37
|
-
config;
|
|
38
|
-
constructor(client, config) {
|
|
39
|
-
this.client = client;
|
|
40
|
-
this.config = config;
|
|
41
|
-
}
|
|
42
|
-
async sendMessage(params) {
|
|
43
|
-
// Merge budget with defaults
|
|
44
|
-
const budget = {
|
|
45
|
-
...this.config.defaultBudget,
|
|
46
|
-
...params.budget
|
|
47
|
-
};
|
|
48
|
-
// Send via official A2A SDK
|
|
49
|
-
const result = await this.client.sendMessage({
|
|
50
|
-
message: {
|
|
51
|
-
kind: 'message',
|
|
52
|
-
messageId: this.generateId(),
|
|
53
|
-
role: params.message.role,
|
|
54
|
-
parts: params.message.parts
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
// Wrap result with LAFS envelope support
|
|
58
|
-
return new LafsA2AResult(result, budget);
|
|
59
|
-
}
|
|
60
|
-
generateId() {
|
|
61
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
62
|
-
const r = Math.random() * 16 | 0;
|
|
63
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
64
|
-
return v.toString(16);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
15
|
export class LafsA2AResult {
|
|
69
16
|
result;
|
|
70
|
-
|
|
71
|
-
|
|
17
|
+
config;
|
|
18
|
+
requestId;
|
|
19
|
+
constructor(result, config, requestId) {
|
|
72
20
|
this.result = result;
|
|
73
|
-
this.
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.requestId = requestId;
|
|
74
23
|
}
|
|
75
24
|
/**
|
|
76
|
-
* Get the
|
|
25
|
+
* Get the raw A2A response
|
|
77
26
|
*/
|
|
78
27
|
getA2AResult() {
|
|
79
28
|
return this.result;
|
|
@@ -94,43 +43,133 @@ export class LafsA2AResult {
|
|
|
94
43
|
return null;
|
|
95
44
|
}
|
|
96
45
|
/**
|
|
97
|
-
*
|
|
46
|
+
* Get success result
|
|
98
47
|
*/
|
|
99
|
-
|
|
100
|
-
if (this.isError()) {
|
|
101
|
-
return
|
|
48
|
+
getSuccess() {
|
|
49
|
+
if (!this.isError()) {
|
|
50
|
+
return this.result;
|
|
102
51
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract Task from response (if present)
|
|
56
|
+
*/
|
|
57
|
+
getTask() {
|
|
58
|
+
const success = this.getSuccess();
|
|
59
|
+
if (!success)
|
|
106
60
|
return null;
|
|
61
|
+
// Check if result is a Task (has id, contextId, status)
|
|
62
|
+
const result = success.result;
|
|
63
|
+
if (result && typeof result === 'object') {
|
|
64
|
+
// Task objects have these properties
|
|
65
|
+
if ('id' in result && 'status' in result) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
107
68
|
}
|
|
108
|
-
|
|
109
|
-
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract Message from response (if present)
|
|
73
|
+
*/
|
|
74
|
+
getMessage() {
|
|
75
|
+
const success = this.getSuccess();
|
|
76
|
+
if (!success)
|
|
110
77
|
return null;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return part.data;
|
|
117
|
-
}
|
|
78
|
+
const result = success.result;
|
|
79
|
+
if (result && typeof result === 'object') {
|
|
80
|
+
// Message objects have messageId
|
|
81
|
+
if ('messageId' in result && !('status' in result)) {
|
|
82
|
+
return result;
|
|
118
83
|
}
|
|
119
84
|
}
|
|
120
85
|
return null;
|
|
121
86
|
}
|
|
122
87
|
/**
|
|
123
|
-
* Check if
|
|
88
|
+
* Check if response contains a LAFS envelope
|
|
124
89
|
*/
|
|
125
90
|
hasLafsEnvelope() {
|
|
126
91
|
return this.getLafsEnvelope() !== null;
|
|
127
92
|
}
|
|
128
93
|
/**
|
|
129
|
-
*
|
|
94
|
+
* Extract LAFS envelope from A2A artifact
|
|
95
|
+
*
|
|
96
|
+
* A2A agents can return LAFS envelopes in artifacts for structured data.
|
|
97
|
+
* This method extracts the envelope from the first artifact containing one.
|
|
98
|
+
*/
|
|
99
|
+
getLafsEnvelope() {
|
|
100
|
+
const task = this.getTask();
|
|
101
|
+
if (!task?.artifacts?.length)
|
|
102
|
+
return null;
|
|
103
|
+
for (const artifact of task.artifacts) {
|
|
104
|
+
for (const part of artifact.parts) {
|
|
105
|
+
if (this.isDataPart(part)) {
|
|
106
|
+
const data = part.data;
|
|
107
|
+
if (this.isLafsEnvelope(data)) {
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get token estimate from LAFS envelope
|
|
130
117
|
*/
|
|
131
118
|
getTokenEstimate() {
|
|
132
119
|
const envelope = this.getLafsEnvelope();
|
|
133
|
-
|
|
120
|
+
if (!envelope?._meta)
|
|
121
|
+
return null;
|
|
122
|
+
// Access LAFS meta fields
|
|
123
|
+
const meta = envelope._meta;
|
|
124
|
+
return meta._tokenEstimate ?? null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get task status
|
|
128
|
+
*/
|
|
129
|
+
getTaskStatus() {
|
|
130
|
+
return this.getTask()?.status ?? null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get task state
|
|
134
|
+
*/
|
|
135
|
+
getTaskState() {
|
|
136
|
+
return this.getTaskStatus()?.state ?? null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Check if task is in a terminal state
|
|
140
|
+
*/
|
|
141
|
+
isTerminal() {
|
|
142
|
+
const state = this.getTaskState();
|
|
143
|
+
if (!state)
|
|
144
|
+
return false;
|
|
145
|
+
const terminalStates = [
|
|
146
|
+
'completed',
|
|
147
|
+
'failed',
|
|
148
|
+
'canceled',
|
|
149
|
+
'rejected'
|
|
150
|
+
];
|
|
151
|
+
return terminalStates.includes(state);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if task requires input
|
|
155
|
+
*/
|
|
156
|
+
isInputRequired() {
|
|
157
|
+
return this.getTaskState() === 'input-required';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if task requires authentication
|
|
161
|
+
*/
|
|
162
|
+
isAuthRequired() {
|
|
163
|
+
return this.getTaskState() === 'auth-required';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get all artifacts from task
|
|
167
|
+
*/
|
|
168
|
+
getArtifacts() {
|
|
169
|
+
return this.getTask()?.artifacts ?? [];
|
|
170
|
+
}
|
|
171
|
+
isDataPart(part) {
|
|
172
|
+
return part.kind === 'data';
|
|
134
173
|
}
|
|
135
174
|
isLafsEnvelope(data) {
|
|
136
175
|
return (typeof data === 'object' &&
|
|
@@ -140,17 +179,21 @@ export class LafsA2AResult {
|
|
|
140
179
|
'success' in data);
|
|
141
180
|
}
|
|
142
181
|
}
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// LAFS Artifact Creation Helpers
|
|
184
|
+
// ============================================================================
|
|
143
185
|
/**
|
|
144
|
-
* Create a LAFS artifact for A2A
|
|
186
|
+
* Create a LAFS envelope artifact for A2A
|
|
145
187
|
*
|
|
146
188
|
* @example
|
|
147
189
|
* ```typescript
|
|
148
|
-
* const
|
|
190
|
+
* const envelope = createEnvelope({
|
|
149
191
|
* success: true,
|
|
150
192
|
* result: { data: '...' },
|
|
151
193
|
* meta: { operation: 'analysis.run' }
|
|
152
194
|
* });
|
|
153
195
|
*
|
|
196
|
+
* const artifact = createLafsArtifact(envelope);
|
|
154
197
|
* task.artifacts.push(artifact);
|
|
155
198
|
* ```
|
|
156
199
|
*/
|
|
@@ -158,10 +201,48 @@ export function createLafsArtifact(envelope) {
|
|
|
158
201
|
return {
|
|
159
202
|
artifactId: generateId(),
|
|
160
203
|
name: 'lafs_response',
|
|
204
|
+
description: 'LAFS-formatted response envelope',
|
|
161
205
|
parts: [{
|
|
162
206
|
kind: 'data',
|
|
163
|
-
data: envelope
|
|
164
|
-
}]
|
|
207
|
+
data: envelope,
|
|
208
|
+
}],
|
|
209
|
+
metadata: {
|
|
210
|
+
'x-lafs-version': '2.0.0',
|
|
211
|
+
'x-content-type': 'application/vnd.lafs.envelope+json',
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Create a text artifact
|
|
217
|
+
*/
|
|
218
|
+
export function createTextArtifact(text, name = 'text_response') {
|
|
219
|
+
const part = {
|
|
220
|
+
kind: 'text',
|
|
221
|
+
text,
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
artifactId: generateId(),
|
|
225
|
+
name,
|
|
226
|
+
parts: [part],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Create a file artifact
|
|
231
|
+
*/
|
|
232
|
+
export function createFileArtifact(fileUrl, mediaType, filename) {
|
|
233
|
+
const part = {
|
|
234
|
+
kind: 'file',
|
|
235
|
+
file: {
|
|
236
|
+
kind: 'uri',
|
|
237
|
+
uri: fileUrl,
|
|
238
|
+
mimeType: mediaType,
|
|
239
|
+
...(filename && { filename }),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
artifactId: generateId(),
|
|
244
|
+
name: filename || 'file',
|
|
245
|
+
parts: [part],
|
|
165
246
|
};
|
|
166
247
|
}
|
|
167
248
|
function generateId() {
|
|
@@ -171,3 +252,18 @@ function generateId() {
|
|
|
171
252
|
return v.toString(16);
|
|
172
253
|
});
|
|
173
254
|
}
|
|
255
|
+
// ============================================================================
|
|
256
|
+
// Extension Helpers
|
|
257
|
+
// ============================================================================
|
|
258
|
+
/**
|
|
259
|
+
* Check if an extension is required
|
|
260
|
+
*/
|
|
261
|
+
export function isExtensionRequired(agentCard, extensionUri) {
|
|
262
|
+
return agentCard.capabilities?.extensions?.some(ext => ext.uri === extensionUri && ext.required) ?? false;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get extension parameters
|
|
266
|
+
*/
|
|
267
|
+
export function getExtensionParams(agentCard, extensionUri) {
|
|
268
|
+
return agentCard.capabilities?.extensions?.find(ext => ext.uri === extensionUri)?.params;
|
|
269
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Extensions Support
|
|
3
|
+
*
|
|
4
|
+
* Extension negotiation, LAFS extension builder, and Express middleware
|
|
5
|
+
* for A2A Protocol v1.0+ compliance.
|
|
6
|
+
*
|
|
7
|
+
* Reference: specs/external/extensions.md, A2A spec Section 3.2.6
|
|
8
|
+
*/
|
|
9
|
+
import type { AgentExtension } from '@a2a-js/sdk';
|
|
10
|
+
import type { RequestHandler } from 'express';
|
|
11
|
+
/** Canonical LAFS extension URI */
|
|
12
|
+
export declare const LAFS_EXTENSION_URI = "https://lafs.dev/extensions/envelope/v1";
|
|
13
|
+
/** Canonical A2A Extensions header per spec Section 3.2.6 */
|
|
14
|
+
export declare const A2A_EXTENSIONS_HEADER = "A2A-Extensions";
|
|
15
|
+
/** LAFS extension parameters declared in Agent Card */
|
|
16
|
+
export interface LafsExtensionParams {
|
|
17
|
+
supportsContextLedger: boolean;
|
|
18
|
+
supportsTokenBudgets: boolean;
|
|
19
|
+
envelopeSchema: string;
|
|
20
|
+
}
|
|
21
|
+
/** Result of extension negotiation between client and agent */
|
|
22
|
+
export interface ExtensionNegotiationResult {
|
|
23
|
+
/** URIs requested by the client */
|
|
24
|
+
requested: string[];
|
|
25
|
+
/** URIs that matched agent-declared extensions */
|
|
26
|
+
activated: string[];
|
|
27
|
+
/** Requested URIs not declared by the agent (ignored per spec) */
|
|
28
|
+
unsupported: string[];
|
|
29
|
+
/** Agent-required URIs not present in client request */
|
|
30
|
+
missingRequired: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse A2A-Extensions header value into URI array.
|
|
34
|
+
* Splits comma-separated URIs, trims whitespace, removes empty strings.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseExtensionsHeader(headerValue: string | undefined): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Negotiate extensions between client-requested and agent-declared sets.
|
|
39
|
+
* Unsupported extensions are ignored per spec. Required agent extensions
|
|
40
|
+
* not requested by the client are flagged in missingRequired.
|
|
41
|
+
*/
|
|
42
|
+
export declare function negotiateExtensions(requestedUris: string[], agentExtensions: AgentExtension[]): ExtensionNegotiationResult;
|
|
43
|
+
/**
|
|
44
|
+
* Format activated extension URIs into header value.
|
|
45
|
+
* Joins URIs with comma separator.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatExtensionsHeader(activatedUris: string[]): string;
|
|
48
|
+
/** Options for building the LAFS extension declaration */
|
|
49
|
+
export interface BuildLafsExtensionOptions {
|
|
50
|
+
required?: boolean;
|
|
51
|
+
supportsContextLedger?: boolean;
|
|
52
|
+
supportsTokenBudgets?: boolean;
|
|
53
|
+
envelopeSchema?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build an A2A AgentExtension object declaring LAFS support.
|
|
57
|
+
* Suitable for inclusion in Agent Card capabilities.extensions[].
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildLafsExtension(options?: BuildLafsExtensionOptions): AgentExtension;
|
|
60
|
+
/**
|
|
61
|
+
* Error thrown when required A2A extensions are not supported by the client.
|
|
62
|
+
* Code -32008 (not in SDK, which stops at -32007).
|
|
63
|
+
*/
|
|
64
|
+
export declare class ExtensionSupportRequiredError extends Error {
|
|
65
|
+
readonly code: -32008;
|
|
66
|
+
readonly httpStatus: 400;
|
|
67
|
+
readonly grpcStatus: "FAILED_PRECONDITION";
|
|
68
|
+
readonly missingExtensions: string[];
|
|
69
|
+
constructor(missingExtensions: string[]);
|
|
70
|
+
/** Convert to JSON-RPC error object */
|
|
71
|
+
toJSONRPCError(): {
|
|
72
|
+
code: number;
|
|
73
|
+
message: string;
|
|
74
|
+
data: Record<string, unknown>;
|
|
75
|
+
};
|
|
76
|
+
/** Convert to RFC 9457 Problem Details object */
|
|
77
|
+
toProblemDetails(): Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
/** Options for the extension negotiation middleware */
|
|
80
|
+
export interface ExtensionNegotiationMiddlewareOptions {
|
|
81
|
+
/** Agent-declared extensions to negotiate against */
|
|
82
|
+
extensions: AgentExtension[];
|
|
83
|
+
/** Return 400 if required extensions are missing (default: true) */
|
|
84
|
+
enforceRequired?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Express middleware for A2A extension negotiation.
|
|
88
|
+
*
|
|
89
|
+
* Parses A2A-Extensions header (and X-A2A-Extensions for SDK compat),
|
|
90
|
+
* validates against declared extensions, sets response header with
|
|
91
|
+
* activated extensions, attaches result to res.locals.a2aExtensions.
|
|
92
|
+
*/
|
|
93
|
+
export declare function extensionNegotiationMiddleware(options: ExtensionNegotiationMiddlewareOptions): RequestHandler;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Extensions Support
|
|
3
|
+
*
|
|
4
|
+
* Extension negotiation, LAFS extension builder, and Express middleware
|
|
5
|
+
* for A2A Protocol v1.0+ compliance.
|
|
6
|
+
*
|
|
7
|
+
* Reference: specs/external/extensions.md, A2A spec Section 3.2.6
|
|
8
|
+
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/** Canonical LAFS extension URI */
|
|
13
|
+
export const LAFS_EXTENSION_URI = 'https://lafs.dev/extensions/envelope/v1';
|
|
14
|
+
/** Canonical A2A Extensions header per spec Section 3.2.6 */
|
|
15
|
+
export const A2A_EXTENSIONS_HEADER = 'A2A-Extensions';
|
|
16
|
+
/**
|
|
17
|
+
* SDK header name (differs from spec).
|
|
18
|
+
* Middleware checks both for SDK compatibility.
|
|
19
|
+
*/
|
|
20
|
+
const SDK_EXTENSIONS_HEADER = 'x-a2a-extensions';
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Functions
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Parse A2A-Extensions header value into URI array.
|
|
26
|
+
* Splits comma-separated URIs, trims whitespace, removes empty strings.
|
|
27
|
+
*/
|
|
28
|
+
export function parseExtensionsHeader(headerValue) {
|
|
29
|
+
if (!headerValue)
|
|
30
|
+
return [];
|
|
31
|
+
return headerValue
|
|
32
|
+
.split(',')
|
|
33
|
+
.map(uri => uri.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Negotiate extensions between client-requested and agent-declared sets.
|
|
38
|
+
* Unsupported extensions are ignored per spec. Required agent extensions
|
|
39
|
+
* not requested by the client are flagged in missingRequired.
|
|
40
|
+
*/
|
|
41
|
+
export function negotiateExtensions(requestedUris, agentExtensions) {
|
|
42
|
+
const declared = new Map(agentExtensions.map(ext => [ext.uri, ext]));
|
|
43
|
+
const activated = [];
|
|
44
|
+
const unsupported = [];
|
|
45
|
+
for (const uri of requestedUris) {
|
|
46
|
+
if (declared.has(uri)) {
|
|
47
|
+
activated.push(uri);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
unsupported.push(uri);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const requestedSet = new Set(requestedUris);
|
|
54
|
+
const missingRequired = [];
|
|
55
|
+
for (const ext of agentExtensions) {
|
|
56
|
+
if (ext.required && !requestedSet.has(ext.uri)) {
|
|
57
|
+
missingRequired.push(ext.uri);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { requested: requestedUris, activated, unsupported, missingRequired };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Format activated extension URIs into header value.
|
|
64
|
+
* Joins URIs with comma separator.
|
|
65
|
+
*/
|
|
66
|
+
export function formatExtensionsHeader(activatedUris) {
|
|
67
|
+
return activatedUris.join(',');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build an A2A AgentExtension object declaring LAFS support.
|
|
71
|
+
* Suitable for inclusion in Agent Card capabilities.extensions[].
|
|
72
|
+
*/
|
|
73
|
+
export function buildLafsExtension(options) {
|
|
74
|
+
return {
|
|
75
|
+
uri: LAFS_EXTENSION_URI,
|
|
76
|
+
description: 'LAFS envelope protocol for structured agent responses',
|
|
77
|
+
required: options?.required ?? false,
|
|
78
|
+
params: {
|
|
79
|
+
supportsContextLedger: options?.supportsContextLedger ?? false,
|
|
80
|
+
supportsTokenBudgets: options?.supportsTokenBudgets ?? false,
|
|
81
|
+
envelopeSchema: options?.envelopeSchema ?? 'https://lafs.dev/schemas/v1/envelope.schema.json',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Error Class
|
|
87
|
+
// ============================================================================
|
|
88
|
+
/**
|
|
89
|
+
* Error thrown when required A2A extensions are not supported by the client.
|
|
90
|
+
* Code -32008 (not in SDK, which stops at -32007).
|
|
91
|
+
*/
|
|
92
|
+
export class ExtensionSupportRequiredError extends Error {
|
|
93
|
+
code = -32008;
|
|
94
|
+
httpStatus = 400;
|
|
95
|
+
grpcStatus = 'FAILED_PRECONDITION';
|
|
96
|
+
missingExtensions;
|
|
97
|
+
constructor(missingExtensions) {
|
|
98
|
+
super(`Required extensions not supported: ${missingExtensions.join(', ')}`);
|
|
99
|
+
this.name = 'ExtensionSupportRequiredError';
|
|
100
|
+
this.missingExtensions = missingExtensions;
|
|
101
|
+
}
|
|
102
|
+
/** Convert to JSON-RPC error object */
|
|
103
|
+
toJSONRPCError() {
|
|
104
|
+
return {
|
|
105
|
+
code: this.code,
|
|
106
|
+
message: this.message,
|
|
107
|
+
data: { missingExtensions: this.missingExtensions },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Convert to RFC 9457 Problem Details object */
|
|
111
|
+
toProblemDetails() {
|
|
112
|
+
return {
|
|
113
|
+
type: 'https://a2a-protocol.org/errors/extension-support-required',
|
|
114
|
+
title: 'Extension Support Required',
|
|
115
|
+
status: this.httpStatus,
|
|
116
|
+
detail: this.message,
|
|
117
|
+
missingExtensions: this.missingExtensions,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Express middleware for A2A extension negotiation.
|
|
123
|
+
*
|
|
124
|
+
* Parses A2A-Extensions header (and X-A2A-Extensions for SDK compat),
|
|
125
|
+
* validates against declared extensions, sets response header with
|
|
126
|
+
* activated extensions, attaches result to res.locals.a2aExtensions.
|
|
127
|
+
*/
|
|
128
|
+
export function extensionNegotiationMiddleware(options) {
|
|
129
|
+
const { extensions, enforceRequired = true } = options;
|
|
130
|
+
return function extensionNegotiationHandler(req, res, next) {
|
|
131
|
+
// Check both canonical and SDK headers (Express normalizes to lowercase)
|
|
132
|
+
const headerValue = req.headers[A2A_EXTENSIONS_HEADER.toLowerCase()] ??
|
|
133
|
+
req.headers[SDK_EXTENSIONS_HEADER];
|
|
134
|
+
const requested = parseExtensionsHeader(headerValue);
|
|
135
|
+
const result = negotiateExtensions(requested, extensions);
|
|
136
|
+
if (enforceRequired && result.missingRequired.length > 0) {
|
|
137
|
+
const error = new ExtensionSupportRequiredError(result.missingRequired);
|
|
138
|
+
res.status(error.httpStatus).json(error.toProblemDetails());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (result.activated.length > 0) {
|
|
142
|
+
res.setHeader(A2A_EXTENSIONS_HEADER, formatExtensionsHeader(result.activated));
|
|
143
|
+
}
|
|
144
|
+
res.locals['a2aExtensions'] = result;
|
|
145
|
+
next();
|
|
146
|
+
};
|
|
147
|
+
}
|
package/dist/src/a2a/index.d.ts
CHANGED
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LAFS Agent-to-Agent (A2A) Integration
|
|
2
|
+
* LAFS Agent-to-Agent (A2A) Integration v2.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Full integration with the official @a2a-js/sdk for Agent-to-Agent communication.
|
|
5
|
+
* Implements A2A Protocol v1.0+ specification.
|
|
6
|
+
*
|
|
7
|
+
* Reference: specs/external/specification.md
|
|
6
8
|
*
|
|
7
9
|
* @example
|
|
8
10
|
* ```typescript
|
|
11
|
+
* import type { AgentCard, Task } from '@cleocode/lafs-protocol/a2a';
|
|
12
|
+
* import {
|
|
13
|
+
* createLafsArtifact,
|
|
14
|
+
* createTextArtifact,
|
|
15
|
+
* LafsA2AResult,
|
|
16
|
+
* isExtensionRequired
|
|
17
|
+
* } from '@cleocode/lafs-protocol/a2a';
|
|
18
|
+
*
|
|
19
|
+
* // Use A2A SDK directly for client operations
|
|
9
20
|
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
10
|
-
* import { withLafsEnvelope } from '@cleocode/lafs-protocol/a2a';
|
|
11
21
|
*
|
|
12
|
-
* // Create official A2A client
|
|
13
22
|
* const factory = new ClientFactory();
|
|
14
|
-
* const
|
|
15
|
-
*
|
|
16
|
-
* // Wrap with LAFS support
|
|
17
|
-
* const client = withLafsEnvelope(a2aClient, {
|
|
18
|
-
* defaultBudget: { maxTokens: 4000 }
|
|
19
|
-
* });
|
|
20
|
-
*
|
|
21
|
-
* // Send message
|
|
22
|
-
* const result = await client.sendMessage({
|
|
23
|
-
* message: {
|
|
24
|
-
* role: 'user',
|
|
25
|
-
* parts: [{ text: 'Analyze data' }]
|
|
26
|
-
* }
|
|
27
|
-
* });
|
|
23
|
+
* const client = await factory.createFromUrl('https://agent.example.com');
|
|
24
|
+
* const result = await client.sendMessage({...});
|
|
28
25
|
*
|
|
29
|
-
* //
|
|
30
|
-
* const
|
|
31
|
-
*
|
|
32
|
-
* console.log(envelope._meta._tokenEstimate);
|
|
33
|
-
* }
|
|
26
|
+
* // Wrap result with LAFS helpers
|
|
27
|
+
* const lafsResult = new LafsA2AResult(result, {}, 'req-001');
|
|
28
|
+
* const envelope = lafsResult.getLafsEnvelope();
|
|
34
29
|
* ```
|
|
35
30
|
*/
|
|
36
|
-
export {
|
|
31
|
+
export { LafsA2AResult, createLafsArtifact, createTextArtifact, createFileArtifact, isExtensionRequired, getExtensionParams, AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, } from './bridge.js';
|
|
32
|
+
export { LAFS_EXTENSION_URI, A2A_EXTENSIONS_HEADER, parseExtensionsHeader, negotiateExtensions, formatExtensionsHeader, buildLafsExtension, ExtensionSupportRequiredError, extensionNegotiationMiddleware, } from './extensions.js';
|
|
33
|
+
export type { LafsExtensionParams, ExtensionNegotiationResult, BuildLafsExtensionOptions, ExtensionNegotiationMiddlewareOptions, } from './extensions.js';
|
|
34
|
+
export { TERMINAL_STATES, INTERRUPTED_STATES, VALID_TRANSITIONS, isValidTransition, isTerminalState, isInterruptedState, InvalidStateTransitionError, TaskImmutabilityError, TaskNotFoundError, TaskManager, attachLafsEnvelope, } from './task-lifecycle.js';
|
|
35
|
+
export type { CreateTaskOptions, ListTasksOptions, ListTasksResult, } from './task-lifecycle.js';
|
|
36
|
+
export * from './bindings/index.js';
|
|
37
|
+
export type { LafsA2AConfig, LafsSendMessageParams, } from './bridge.js';
|
|
38
|
+
export type { Task, TaskState, TaskStatus, Artifact, Part, Message, AgentCard, AgentSkill, AgentCapabilities, AgentExtension, PushNotificationConfig, MessageSendConfiguration, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, SendMessageResponse, SendMessageSuccessResponse, JSONRPCErrorResponse, TextPart, DataPart, FilePart, } from '@a2a-js/sdk';
|