@cleocode/lafs 1.8.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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/schemas/v1/conformance-profiles.json +39 -0
- package/dist/schemas/v1/envelope.schema.json +306 -0
- package/dist/schemas/v1/error-registry.json +162 -0
- 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 +102 -0
- package/dist/src/a2a/bindings/http.js +120 -0
- package/dist/src/a2a/bindings/index.d.ts +35 -0
- package/dist/src/a2a/bindings/index.js +79 -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 +175 -0
- package/dist/src/a2a/bridge.js +286 -0
- package/dist/src/a2a/extensions.d.ts +121 -0
- package/dist/src/a2a/extensions.js +205 -0
- package/dist/src/a2a/index.d.ts +40 -0
- package/dist/src/a2a/index.js +76 -0
- package/dist/src/a2a/streaming.d.ts +74 -0
- package/dist/src/a2a/streaming.js +265 -0
- package/dist/src/a2a/task-lifecycle.d.ts +109 -0
- package/dist/src/a2a/task-lifecycle.js +313 -0
- package/dist/src/budgetEnforcement.d.ts +84 -0
- package/dist/src/budgetEnforcement.js +328 -0
- package/dist/src/circuit-breaker/index.d.ts +121 -0
- package/dist/src/circuit-breaker/index.js +249 -0
- package/dist/src/cli.d.ts +16 -0
- package/dist/src/cli.js +63 -0
- package/dist/src/compliance.d.ts +31 -0
- package/dist/src/compliance.js +89 -0
- package/dist/src/conformance.d.ts +7 -0
- package/dist/src/conformance.js +248 -0
- package/dist/src/conformanceProfiles.d.ts +11 -0
- package/dist/src/conformanceProfiles.js +34 -0
- package/dist/src/deprecationRegistry.d.ts +13 -0
- package/dist/src/deprecationRegistry.js +39 -0
- package/dist/src/discovery.d.ts +286 -0
- package/dist/src/discovery.js +350 -0
- package/dist/src/envelope.d.ts +60 -0
- package/dist/src/envelope.js +136 -0
- package/dist/src/errorRegistry.d.ts +28 -0
- package/dist/src/errorRegistry.js +36 -0
- package/dist/src/fieldExtraction.d.ts +67 -0
- package/dist/src/fieldExtraction.js +133 -0
- package/dist/src/flagResolver.d.ts +46 -0
- package/dist/src/flagResolver.js +47 -0
- package/dist/src/flagSemantics.d.ts +16 -0
- package/dist/src/flagSemantics.js +45 -0
- package/dist/src/health/index.d.ts +105 -0
- package/dist/src/health/index.js +220 -0
- package/dist/src/index.d.ts +24 -0
- package/dist/src/index.js +34 -0
- package/dist/src/mcpAdapter.d.ts +28 -0
- package/dist/src/mcpAdapter.js +281 -0
- package/dist/src/mviProjection.d.ts +19 -0
- package/dist/src/mviProjection.js +116 -0
- package/dist/src/problemDetails.d.ts +34 -0
- package/dist/src/problemDetails.js +45 -0
- package/dist/src/shutdown/index.d.ts +69 -0
- package/dist/src/shutdown/index.js +160 -0
- package/dist/src/tokenEstimator.d.ts +87 -0
- package/dist/src/tokenEstimator.js +238 -0
- package/dist/src/types.d.ts +135 -0
- package/dist/src/types.js +12 -0
- package/dist/src/validateEnvelope.d.ts +15 -0
- package/dist/src/validateEnvelope.js +31 -0
- package/lafs.md +819 -0
- package/package.json +88 -0
- package/schemas/v1/agent-card.schema.json +230 -0
- package/schemas/v1/conformance-profiles.json +39 -0
- package/schemas/v1/context-ledger.schema.json +70 -0
- package/schemas/v1/discovery.schema.json +132 -0
- package/schemas/v1/envelope.schema.json +306 -0
- package/schemas/v1/error-registry.json +162 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS A2A Bridge v2.0
|
|
3
|
+
*
|
|
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
|
|
8
|
+
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Imports - Use official A2A SDK types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
import { AGENT_CARD_PATH, HTTP_EXTENSION_HEADER, } from '@a2a-js/sdk';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Result Wrapper
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Wrapper for A2A responses with LAFS envelope support
|
|
18
|
+
*/
|
|
19
|
+
export class LafsA2AResult {
|
|
20
|
+
result;
|
|
21
|
+
config;
|
|
22
|
+
requestId;
|
|
23
|
+
constructor(result, config, requestId) {
|
|
24
|
+
this.result = result;
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.requestId = requestId;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the raw A2A response
|
|
30
|
+
*/
|
|
31
|
+
getA2AResult() {
|
|
32
|
+
return this.result;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if result is an error
|
|
36
|
+
*/
|
|
37
|
+
isError() {
|
|
38
|
+
return 'error' in this.result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get error details if result is an error
|
|
42
|
+
*/
|
|
43
|
+
getError() {
|
|
44
|
+
if (this.isError()) {
|
|
45
|
+
return this.result;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get success result
|
|
51
|
+
*/
|
|
52
|
+
getSuccess() {
|
|
53
|
+
if (!this.isError()) {
|
|
54
|
+
return this.result;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract Task from response (if present)
|
|
60
|
+
*/
|
|
61
|
+
getTask() {
|
|
62
|
+
const success = this.getSuccess();
|
|
63
|
+
if (!success)
|
|
64
|
+
return null;
|
|
65
|
+
// Check if result is a Task (has id, contextId, status)
|
|
66
|
+
const result = success.result;
|
|
67
|
+
if (result && typeof result === 'object') {
|
|
68
|
+
// Task objects have these properties
|
|
69
|
+
if ('id' in result && 'status' in result) {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract Message from response (if present)
|
|
77
|
+
*/
|
|
78
|
+
getMessage() {
|
|
79
|
+
const success = this.getSuccess();
|
|
80
|
+
if (!success)
|
|
81
|
+
return null;
|
|
82
|
+
const result = success.result;
|
|
83
|
+
if (result && typeof result === 'object') {
|
|
84
|
+
// Message objects have messageId
|
|
85
|
+
if ('messageId' in result && !('status' in result)) {
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if response contains a LAFS envelope
|
|
93
|
+
*/
|
|
94
|
+
hasLafsEnvelope() {
|
|
95
|
+
return this.getLafsEnvelope() !== null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Extract LAFS envelope from A2A artifact
|
|
99
|
+
*
|
|
100
|
+
* A2A agents can return LAFS envelopes in artifacts for structured data.
|
|
101
|
+
* This method extracts the envelope from the first artifact containing one.
|
|
102
|
+
*/
|
|
103
|
+
getLafsEnvelope() {
|
|
104
|
+
const task = this.getTask();
|
|
105
|
+
if (!task?.artifacts?.length)
|
|
106
|
+
return null;
|
|
107
|
+
for (const artifact of task.artifacts) {
|
|
108
|
+
for (const part of artifact.parts) {
|
|
109
|
+
if (this.isDataPart(part)) {
|
|
110
|
+
const data = part.data;
|
|
111
|
+
if (this.isLafsEnvelope(data)) {
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get token estimate from LAFS envelope
|
|
121
|
+
*/
|
|
122
|
+
getTokenEstimate() {
|
|
123
|
+
const envelope = this.getLafsEnvelope();
|
|
124
|
+
if (!envelope?._meta)
|
|
125
|
+
return null;
|
|
126
|
+
// Access LAFS meta fields
|
|
127
|
+
const meta = envelope._meta;
|
|
128
|
+
return meta._tokenEstimate ?? null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get task status
|
|
132
|
+
*/
|
|
133
|
+
getTaskStatus() {
|
|
134
|
+
return this.getTask()?.status ?? null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get task state
|
|
138
|
+
*/
|
|
139
|
+
getTaskState() {
|
|
140
|
+
return this.getTaskStatus()?.state ?? null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if task is in a terminal state
|
|
144
|
+
*/
|
|
145
|
+
isTerminal() {
|
|
146
|
+
const state = this.getTaskState();
|
|
147
|
+
if (!state)
|
|
148
|
+
return false;
|
|
149
|
+
const terminalStates = [
|
|
150
|
+
'completed',
|
|
151
|
+
'failed',
|
|
152
|
+
'canceled',
|
|
153
|
+
'rejected'
|
|
154
|
+
];
|
|
155
|
+
return terminalStates.includes(state);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if task requires input
|
|
159
|
+
*/
|
|
160
|
+
isInputRequired() {
|
|
161
|
+
return this.getTaskState() === 'input-required';
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if task requires authentication
|
|
165
|
+
*/
|
|
166
|
+
isAuthRequired() {
|
|
167
|
+
return this.getTaskState() === 'auth-required';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get all artifacts from task
|
|
171
|
+
*/
|
|
172
|
+
getArtifacts() {
|
|
173
|
+
return this.getTask()?.artifacts ?? [];
|
|
174
|
+
}
|
|
175
|
+
isDataPart(part) {
|
|
176
|
+
return part.kind === 'data';
|
|
177
|
+
}
|
|
178
|
+
isLafsEnvelope(data) {
|
|
179
|
+
return (typeof data === 'object' &&
|
|
180
|
+
data !== null &&
|
|
181
|
+
'$schema' in data &&
|
|
182
|
+
'_meta' in data &&
|
|
183
|
+
'success' in data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// LAFS Artifact Creation Helpers
|
|
188
|
+
// ============================================================================
|
|
189
|
+
/**
|
|
190
|
+
* Create a LAFS envelope artifact for A2A
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* const envelope = createEnvelope({
|
|
195
|
+
* success: true,
|
|
196
|
+
* result: { data: '...' },
|
|
197
|
+
* meta: { operation: 'analysis.run' }
|
|
198
|
+
* });
|
|
199
|
+
*
|
|
200
|
+
* const artifact = createLafsArtifact(envelope);
|
|
201
|
+
* task.artifacts.push(artifact);
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export function createLafsArtifact(envelope) {
|
|
205
|
+
return {
|
|
206
|
+
artifactId: generateId(),
|
|
207
|
+
name: 'lafs_response',
|
|
208
|
+
description: 'LAFS-formatted response envelope',
|
|
209
|
+
parts: [{
|
|
210
|
+
kind: 'data',
|
|
211
|
+
data: envelope,
|
|
212
|
+
}],
|
|
213
|
+
metadata: {
|
|
214
|
+
'x-lafs-version': '2.0.0',
|
|
215
|
+
'x-content-type': 'application/vnd.lafs.envelope+json',
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Create a text artifact
|
|
221
|
+
*/
|
|
222
|
+
export function createTextArtifact(text, name = 'text_response') {
|
|
223
|
+
const part = {
|
|
224
|
+
kind: 'text',
|
|
225
|
+
text,
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
artifactId: generateId(),
|
|
229
|
+
name,
|
|
230
|
+
parts: [part],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Create a file artifact
|
|
235
|
+
*/
|
|
236
|
+
export function createFileArtifact(fileUrl, mediaType, filename) {
|
|
237
|
+
const part = {
|
|
238
|
+
kind: 'file',
|
|
239
|
+
file: {
|
|
240
|
+
kind: 'uri',
|
|
241
|
+
uri: fileUrl,
|
|
242
|
+
mimeType: mediaType,
|
|
243
|
+
...(filename && { filename }),
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
artifactId: generateId(),
|
|
248
|
+
name: filename || 'file',
|
|
249
|
+
parts: [part],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function generateId() {
|
|
253
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
254
|
+
const r = Math.random() * 16 | 0;
|
|
255
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
256
|
+
return v.toString(16);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// Extension Helpers
|
|
261
|
+
// ============================================================================
|
|
262
|
+
/**
|
|
263
|
+
* Check if an extension is required
|
|
264
|
+
*/
|
|
265
|
+
export function isExtensionRequired(agentCard, extensionUri) {
|
|
266
|
+
return agentCard.capabilities?.extensions?.some(ext => ext.uri === extensionUri && ext.required) ?? false;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get extension parameters
|
|
270
|
+
*/
|
|
271
|
+
export function getExtensionParams(agentCard, extensionUri) {
|
|
272
|
+
return agentCard.capabilities?.extensions?.find(ext => ext.uri === extensionUri)?.params;
|
|
273
|
+
}
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Constants
|
|
276
|
+
// ============================================================================
|
|
277
|
+
/**
|
|
278
|
+
* A2A Agent Card well-known path
|
|
279
|
+
* Reference: specs/external/agent-discovery.md
|
|
280
|
+
*/
|
|
281
|
+
export { AGENT_CARD_PATH };
|
|
282
|
+
/**
|
|
283
|
+
* HTTP header for A2A Extensions
|
|
284
|
+
* Reference: specs/external/extensions.md
|
|
285
|
+
*/
|
|
286
|
+
export { HTTP_EXTENSION_HEADER };
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
kind?: ExtensionKind;
|
|
21
|
+
}
|
|
22
|
+
export type ExtensionKind = 'data-only' | 'profile' | 'method' | 'state-machine';
|
|
23
|
+
/** Result of extension negotiation between client and agent */
|
|
24
|
+
export interface ExtensionNegotiationResult {
|
|
25
|
+
/** URIs requested by the client */
|
|
26
|
+
requested: string[];
|
|
27
|
+
/** URIs that matched agent-declared extensions */
|
|
28
|
+
activated: string[];
|
|
29
|
+
/** Requested URIs not declared by the agent (ignored per spec) */
|
|
30
|
+
unsupported: string[];
|
|
31
|
+
/** Agent-required URIs not present in client request */
|
|
32
|
+
missingRequired: string[];
|
|
33
|
+
/** Activated extensions grouped by declared kind (when provided) */
|
|
34
|
+
activatedByKind: Partial<Record<ExtensionKind, string[]>>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse A2A-Extensions header value into URI array.
|
|
38
|
+
* Splits comma-separated URIs, trims whitespace, removes empty strings.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseExtensionsHeader(headerValue: string | undefined): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Negotiate extensions between client-requested and agent-declared sets.
|
|
43
|
+
* Unsupported extensions are ignored per spec. Required agent extensions
|
|
44
|
+
* not requested by the client are flagged in missingRequired.
|
|
45
|
+
*/
|
|
46
|
+
export declare function negotiateExtensions(requestedUris: string[], agentExtensions: AgentExtension[]): ExtensionNegotiationResult;
|
|
47
|
+
/**
|
|
48
|
+
* Format activated extension URIs into header value.
|
|
49
|
+
* Joins URIs with comma separator.
|
|
50
|
+
*/
|
|
51
|
+
export declare function formatExtensionsHeader(activatedUris: string[]): string;
|
|
52
|
+
/** Options for building the LAFS extension declaration */
|
|
53
|
+
export interface BuildLafsExtensionOptions {
|
|
54
|
+
required?: boolean;
|
|
55
|
+
supportsContextLedger?: boolean;
|
|
56
|
+
supportsTokenBudgets?: boolean;
|
|
57
|
+
envelopeSchema?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build an A2A AgentExtension object declaring LAFS support.
|
|
61
|
+
* Suitable for inclusion in Agent Card capabilities.extensions[].
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildLafsExtension(options?: BuildLafsExtensionOptions): AgentExtension;
|
|
64
|
+
export interface BuildExtensionOptions {
|
|
65
|
+
uri: string;
|
|
66
|
+
description: string;
|
|
67
|
+
required?: boolean;
|
|
68
|
+
kind: ExtensionKind;
|
|
69
|
+
params?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
export declare function buildExtension(options: BuildExtensionOptions): AgentExtension;
|
|
72
|
+
export declare function isValidExtensionKind(kind: string): kind is ExtensionKind;
|
|
73
|
+
export declare function validateExtensionDeclaration(extension: AgentExtension): {
|
|
74
|
+
valid: boolean;
|
|
75
|
+
error?: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Error thrown when required A2A extensions are not supported by the client.
|
|
79
|
+
* Code -32008 (not in SDK, which stops at -32007).
|
|
80
|
+
*/
|
|
81
|
+
export declare class ExtensionSupportRequiredError extends Error {
|
|
82
|
+
readonly code: -32008;
|
|
83
|
+
readonly httpStatus: 400;
|
|
84
|
+
readonly grpcStatus: "FAILED_PRECONDITION";
|
|
85
|
+
readonly missingExtensions: string[];
|
|
86
|
+
constructor(missingExtensions: string[]);
|
|
87
|
+
/** Convert to JSON-RPC error object */
|
|
88
|
+
toJSONRPCError(): {
|
|
89
|
+
code: number;
|
|
90
|
+
message: string;
|
|
91
|
+
data: Record<string, unknown>;
|
|
92
|
+
};
|
|
93
|
+
/** Convert to RFC 9457 Problem Details object with agent-actionable fields */
|
|
94
|
+
toProblemDetails(): Record<string, unknown> & {
|
|
95
|
+
agentAction: string;
|
|
96
|
+
};
|
|
97
|
+
/** Convert to a LAFSError-compatible object */
|
|
98
|
+
toLafsError(): {
|
|
99
|
+
code: string;
|
|
100
|
+
message: string;
|
|
101
|
+
category: 'CONTRACT';
|
|
102
|
+
retryable: boolean;
|
|
103
|
+
retryAfterMs: null;
|
|
104
|
+
details: Record<string, unknown>;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Options for the extension negotiation middleware */
|
|
108
|
+
export interface ExtensionNegotiationMiddlewareOptions {
|
|
109
|
+
/** Agent-declared extensions to negotiate against */
|
|
110
|
+
extensions: AgentExtension[];
|
|
111
|
+
/** Return 400 if required extensions are missing (default: true) */
|
|
112
|
+
enforceRequired?: boolean;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Express middleware for A2A extension negotiation.
|
|
116
|
+
*
|
|
117
|
+
* Parses A2A-Extensions header (and X-A2A-Extensions for SDK compat),
|
|
118
|
+
* validates against declared extensions, sets response header with
|
|
119
|
+
* activated extensions, attaches result to res.locals.a2aExtensions.
|
|
120
|
+
*/
|
|
121
|
+
export declare function extensionNegotiationMiddleware(options: ExtensionNegotiationMiddlewareOptions): RequestHandler;
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
const VALID_EXTENSION_KINDS = ['data-only', 'profile', 'method', 'state-machine'];
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Functions
|
|
24
|
+
// ============================================================================
|
|
25
|
+
/**
|
|
26
|
+
* Parse A2A-Extensions header value into URI array.
|
|
27
|
+
* Splits comma-separated URIs, trims whitespace, removes empty strings.
|
|
28
|
+
*/
|
|
29
|
+
export function parseExtensionsHeader(headerValue) {
|
|
30
|
+
if (!headerValue)
|
|
31
|
+
return [];
|
|
32
|
+
return headerValue
|
|
33
|
+
.split(',')
|
|
34
|
+
.map(uri => uri.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
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 function negotiateExtensions(requestedUris, agentExtensions) {
|
|
43
|
+
const declared = new Map(agentExtensions.map(ext => [ext.uri, ext]));
|
|
44
|
+
const activated = [];
|
|
45
|
+
const unsupported = [];
|
|
46
|
+
for (const uri of requestedUris) {
|
|
47
|
+
if (declared.has(uri)) {
|
|
48
|
+
activated.push(uri);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
unsupported.push(uri);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const requestedSet = new Set(requestedUris);
|
|
55
|
+
const missingRequired = [];
|
|
56
|
+
for (const ext of agentExtensions) {
|
|
57
|
+
if (ext.required && !requestedSet.has(ext.uri)) {
|
|
58
|
+
missingRequired.push(ext.uri);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const activatedByKind = {};
|
|
62
|
+
for (const uri of activated) {
|
|
63
|
+
const ext = declared.get(uri);
|
|
64
|
+
const kind = ext?.params && typeof ext.params === 'object'
|
|
65
|
+
? ext.params['kind']
|
|
66
|
+
: undefined;
|
|
67
|
+
if (typeof kind === 'string' && VALID_EXTENSION_KINDS.includes(kind)) {
|
|
68
|
+
const typedKind = kind;
|
|
69
|
+
if (!activatedByKind[typedKind]) {
|
|
70
|
+
activatedByKind[typedKind] = [];
|
|
71
|
+
}
|
|
72
|
+
activatedByKind[typedKind].push(uri);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { requested: requestedUris, activated, unsupported, missingRequired, activatedByKind };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Format activated extension URIs into header value.
|
|
79
|
+
* Joins URIs with comma separator.
|
|
80
|
+
*/
|
|
81
|
+
export function formatExtensionsHeader(activatedUris) {
|
|
82
|
+
return activatedUris.join(',');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build an A2A AgentExtension object declaring LAFS support.
|
|
86
|
+
* Suitable for inclusion in Agent Card capabilities.extensions[].
|
|
87
|
+
*/
|
|
88
|
+
export function buildLafsExtension(options) {
|
|
89
|
+
return {
|
|
90
|
+
uri: LAFS_EXTENSION_URI,
|
|
91
|
+
description: 'LAFS envelope protocol for structured agent responses',
|
|
92
|
+
required: options?.required ?? false,
|
|
93
|
+
params: {
|
|
94
|
+
supportsContextLedger: options?.supportsContextLedger ?? false,
|
|
95
|
+
supportsTokenBudgets: options?.supportsTokenBudgets ?? false,
|
|
96
|
+
envelopeSchema: options?.envelopeSchema ?? 'https://lafs.dev/schemas/v1/envelope.schema.json',
|
|
97
|
+
kind: 'profile',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function buildExtension(options) {
|
|
102
|
+
return {
|
|
103
|
+
uri: options.uri,
|
|
104
|
+
description: options.description,
|
|
105
|
+
required: options.required ?? false,
|
|
106
|
+
params: {
|
|
107
|
+
kind: options.kind,
|
|
108
|
+
...(options.params ?? {}),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function isValidExtensionKind(kind) {
|
|
113
|
+
return VALID_EXTENSION_KINDS.includes(kind);
|
|
114
|
+
}
|
|
115
|
+
export function validateExtensionDeclaration(extension) {
|
|
116
|
+
const kind = extension.params && typeof extension.params === 'object'
|
|
117
|
+
? extension.params['kind']
|
|
118
|
+
: undefined;
|
|
119
|
+
if (kind === undefined) {
|
|
120
|
+
return { valid: true };
|
|
121
|
+
}
|
|
122
|
+
if (typeof kind !== 'string' || !isValidExtensionKind(kind)) {
|
|
123
|
+
return { valid: false, error: `invalid extension kind: ${String(kind)}` };
|
|
124
|
+
}
|
|
125
|
+
return { valid: true };
|
|
126
|
+
}
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Error Class
|
|
129
|
+
// ============================================================================
|
|
130
|
+
/**
|
|
131
|
+
* Error thrown when required A2A extensions are not supported by the client.
|
|
132
|
+
* Code -32008 (not in SDK, which stops at -32007).
|
|
133
|
+
*/
|
|
134
|
+
export class ExtensionSupportRequiredError extends Error {
|
|
135
|
+
code = -32008;
|
|
136
|
+
httpStatus = 400;
|
|
137
|
+
grpcStatus = 'FAILED_PRECONDITION';
|
|
138
|
+
missingExtensions;
|
|
139
|
+
constructor(missingExtensions) {
|
|
140
|
+
super(`Required extensions not supported: ${missingExtensions.join(', ')}`);
|
|
141
|
+
this.name = 'ExtensionSupportRequiredError';
|
|
142
|
+
this.missingExtensions = missingExtensions;
|
|
143
|
+
}
|
|
144
|
+
/** Convert to JSON-RPC error object */
|
|
145
|
+
toJSONRPCError() {
|
|
146
|
+
return {
|
|
147
|
+
code: this.code,
|
|
148
|
+
message: this.message,
|
|
149
|
+
data: { missingExtensions: this.missingExtensions },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Convert to RFC 9457 Problem Details object with agent-actionable fields */
|
|
153
|
+
toProblemDetails() {
|
|
154
|
+
return {
|
|
155
|
+
type: 'https://a2a-protocol.org/errors/extension-support-required',
|
|
156
|
+
title: 'Extension Support Required',
|
|
157
|
+
status: this.httpStatus,
|
|
158
|
+
detail: this.message,
|
|
159
|
+
missingExtensions: this.missingExtensions,
|
|
160
|
+
agentAction: 'retry_modified',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Convert to a LAFSError-compatible object */
|
|
164
|
+
toLafsError() {
|
|
165
|
+
return {
|
|
166
|
+
code: 'E_CONTRACT_EXTENSION_REQUIRED',
|
|
167
|
+
message: this.message,
|
|
168
|
+
category: 'CONTRACT',
|
|
169
|
+
retryable: true,
|
|
170
|
+
retryAfterMs: null,
|
|
171
|
+
details: {
|
|
172
|
+
missingExtensions: this.missingExtensions,
|
|
173
|
+
agentAction: 'retry_modified',
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Express middleware for A2A extension negotiation.
|
|
180
|
+
*
|
|
181
|
+
* Parses A2A-Extensions header (and X-A2A-Extensions for SDK compat),
|
|
182
|
+
* validates against declared extensions, sets response header with
|
|
183
|
+
* activated extensions, attaches result to res.locals.a2aExtensions.
|
|
184
|
+
*/
|
|
185
|
+
export function extensionNegotiationMiddleware(options) {
|
|
186
|
+
const { extensions, enforceRequired = true } = options;
|
|
187
|
+
return function extensionNegotiationHandler(req, res, next) {
|
|
188
|
+
// Check both canonical and SDK headers (Express normalizes to lowercase)
|
|
189
|
+
const headerValue = req.headers[A2A_EXTENSIONS_HEADER.toLowerCase()] ??
|
|
190
|
+
req.headers[SDK_EXTENSIONS_HEADER];
|
|
191
|
+
const requested = parseExtensionsHeader(headerValue);
|
|
192
|
+
const result = negotiateExtensions(requested, extensions);
|
|
193
|
+
if (enforceRequired && result.missingRequired.length > 0) {
|
|
194
|
+
const error = new ExtensionSupportRequiredError(result.missingRequired);
|
|
195
|
+
res.setHeader('Content-Type', 'application/problem+json');
|
|
196
|
+
res.status(error.httpStatus).json(error.toProblemDetails());
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (result.activated.length > 0) {
|
|
200
|
+
res.setHeader(A2A_EXTENSIONS_HEADER, formatExtensionsHeader(result.activated));
|
|
201
|
+
}
|
|
202
|
+
res.locals['a2aExtensions'] = result;
|
|
203
|
+
next();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Agent-to-Agent (A2A) Integration v2.0
|
|
3
|
+
*
|
|
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
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import type { AgentCard, Task } from '@cleocode/lafs/a2a';
|
|
12
|
+
* import {
|
|
13
|
+
* createLafsArtifact,
|
|
14
|
+
* createTextArtifact,
|
|
15
|
+
* LafsA2AResult,
|
|
16
|
+
* isExtensionRequired
|
|
17
|
+
* } from '@cleocode/lafs/a2a';
|
|
18
|
+
*
|
|
19
|
+
* // Use A2A SDK directly for client operations
|
|
20
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
21
|
+
*
|
|
22
|
+
* const factory = new ClientFactory();
|
|
23
|
+
* const client = await factory.createFromUrl('https://agent.example.com');
|
|
24
|
+
* const result = await client.sendMessage({...});
|
|
25
|
+
*
|
|
26
|
+
* // Wrap result with LAFS helpers
|
|
27
|
+
* const lafsResult = new LafsA2AResult(result, {}, 'req-001');
|
|
28
|
+
* const envelope = lafsResult.getLafsEnvelope();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
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, TaskRefinementError, TaskManager, attachLafsEnvelope, } from './task-lifecycle.js';
|
|
35
|
+
export { TaskEventBus, PushNotificationConfigStore, PushNotificationDispatcher, TaskArtifactAssembler, streamTaskEvents, } from './streaming.js';
|
|
36
|
+
export type { TaskStreamEvent, StreamIteratorOptions, PushNotificationDeliveryResult, PushTransport, } from './streaming.js';
|
|
37
|
+
export type { CreateTaskOptions, ListTasksOptions, ListTasksResult, } from './task-lifecycle.js';
|
|
38
|
+
export * from './bindings/index.js';
|
|
39
|
+
export type { LafsA2AConfig, LafsSendMessageParams, } from './bridge.js';
|
|
40
|
+
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';
|