@cleocode/lafs-protocol 0.5.0 → 1.1.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 +0 -0
- package/README.md +7 -3
- package/dist/examples/discovery-server.d.ts +8 -0
- package/dist/examples/discovery-server.js +216 -0
- package/dist/examples/mcp-lafs-client.d.ts +10 -0
- package/dist/examples/mcp-lafs-client.js +427 -0
- package/dist/examples/mcp-lafs-server.d.ts +10 -0
- package/dist/examples/mcp-lafs-server.js +358 -0
- package/dist/schemas/v1/envelope.schema.json +0 -0
- package/dist/schemas/v1/error-registry.json +0 -0
- package/dist/src/a2a/bridge.d.ts +129 -0
- package/dist/src/a2a/bridge.js +173 -0
- package/dist/src/a2a/index.d.ts +36 -0
- package/dist/src/a2a/index.js +36 -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 +0 -0
- package/dist/src/cli.js +0 -0
- package/dist/src/conformance.d.ts +0 -0
- package/dist/src/conformance.js +0 -0
- package/dist/src/discovery.d.ts +127 -0
- package/dist/src/discovery.js +304 -0
- package/dist/src/errorRegistry.d.ts +0 -0
- package/dist/src/errorRegistry.js +0 -0
- package/dist/src/flagSemantics.d.ts +0 -0
- package/dist/src/flagSemantics.js +0 -0
- package/dist/src/health/index.d.ts +105 -0
- package/dist/src/health/index.js +211 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +10 -0
- package/dist/src/mcpAdapter.d.ts +28 -0
- package/dist/src/mcpAdapter.js +281 -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 +25 -0
- package/dist/src/types.js +0 -0
- package/dist/src/validateEnvelope.d.ts +0 -0
- package/dist/src/validateEnvelope.js +0 -0
- package/lafs.md +167 -0
- package/package.json +10 -4
- package/schemas/v1/context-ledger.schema.json +0 -0
- package/schemas/v1/discovery.schema.json +132 -0
- package/schemas/v1/envelope.schema.json +0 -0
- package/schemas/v1/error-registry.json +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP-LAFS Server Example
|
|
4
|
+
*
|
|
5
|
+
* A working MCP server that wraps all tool responses in LAFS-compliant envelopes.
|
|
6
|
+
* Demonstrates how LAFS complements MCP by adding structured metadata and budget enforcement.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx ts-node examples/mcp-lafs-server.ts
|
|
9
|
+
*/
|
|
10
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import { wrapMCPResult } from "../src/mcpAdapter.js";
|
|
14
|
+
const simulatedDatabase = new Map([
|
|
15
|
+
["1", { id: "1", name: "Product A", value: 100, createdAt: new Date().toISOString() }],
|
|
16
|
+
["2", { id: "2", name: "Product B", value: 200, createdAt: new Date().toISOString() }],
|
|
17
|
+
["3", { id: "3", name: "Product C", value: 300, createdAt: new Date().toISOString() }],
|
|
18
|
+
]);
|
|
19
|
+
// Tool definitions
|
|
20
|
+
const TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "weather",
|
|
23
|
+
description: "Get current weather for a location",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
location: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "City name or coordinates",
|
|
30
|
+
},
|
|
31
|
+
units: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["celsius", "fahrenheit"],
|
|
34
|
+
description: "Temperature units",
|
|
35
|
+
default: "celsius",
|
|
36
|
+
},
|
|
37
|
+
_budget: {
|
|
38
|
+
type: "number",
|
|
39
|
+
description: "Token budget for response (LAFS extension)",
|
|
40
|
+
minimum: 10,
|
|
41
|
+
maximum: 10000,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ["location"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "calculator",
|
|
49
|
+
description: "Perform mathematical calculations",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
operation: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: ["add", "subtract", "multiply", "divide", "power", "sqrt"],
|
|
56
|
+
description: "Mathematical operation to perform",
|
|
57
|
+
},
|
|
58
|
+
a: {
|
|
59
|
+
type: "number",
|
|
60
|
+
description: "First operand",
|
|
61
|
+
},
|
|
62
|
+
b: {
|
|
63
|
+
type: "number",
|
|
64
|
+
description: "Second operand (not needed for sqrt)",
|
|
65
|
+
},
|
|
66
|
+
_budget: {
|
|
67
|
+
type: "number",
|
|
68
|
+
description: "Token budget for response (LAFS extension)",
|
|
69
|
+
minimum: 10,
|
|
70
|
+
maximum: 1000,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["operation", "a"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "database_query",
|
|
78
|
+
description: "Query the simulated database",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
action: {
|
|
83
|
+
type: "string",
|
|
84
|
+
enum: ["get", "list", "search"],
|
|
85
|
+
description: "Query action to perform",
|
|
86
|
+
},
|
|
87
|
+
id: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Record ID (for get action)",
|
|
90
|
+
},
|
|
91
|
+
query: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Search query (for search action)",
|
|
94
|
+
},
|
|
95
|
+
limit: {
|
|
96
|
+
type: "number",
|
|
97
|
+
description: "Maximum results to return",
|
|
98
|
+
default: 10,
|
|
99
|
+
},
|
|
100
|
+
_budget: {
|
|
101
|
+
type: "number",
|
|
102
|
+
description: "Token budget for response (LAFS extension)",
|
|
103
|
+
minimum: 10,
|
|
104
|
+
maximum: 5000,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: ["action"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
// Weather simulation
|
|
112
|
+
async function getWeather(location, units) {
|
|
113
|
+
// Simulate API call delay
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
115
|
+
// Generate deterministic but varied weather based on location
|
|
116
|
+
const hash = location.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
117
|
+
const conditions = ["sunny", "cloudy", "rainy", "partly cloudy", "clear"];
|
|
118
|
+
const condition = conditions[hash % conditions.length];
|
|
119
|
+
// Temperature based on condition and some randomness
|
|
120
|
+
let baseTemp = 20; // celsius
|
|
121
|
+
if (condition === "sunny")
|
|
122
|
+
baseTemp = 25;
|
|
123
|
+
if (condition === "rainy")
|
|
124
|
+
baseTemp = 15;
|
|
125
|
+
if (condition === "clear")
|
|
126
|
+
baseTemp = 22;
|
|
127
|
+
const tempC = baseTemp + (hash % 10) - 5;
|
|
128
|
+
const tempF = Math.round((tempC * 9) / 5 + 32);
|
|
129
|
+
return {
|
|
130
|
+
location,
|
|
131
|
+
temperature: units === "fahrenheit" ? tempF : tempC,
|
|
132
|
+
temperatureUnit: units,
|
|
133
|
+
conditions: condition,
|
|
134
|
+
humidity: 40 + (hash % 50),
|
|
135
|
+
windSpeed: 5 + (hash % 20),
|
|
136
|
+
windUnit: "km/h",
|
|
137
|
+
forecast: [
|
|
138
|
+
{ day: "Today", high: tempC + 2, low: tempC - 3, condition },
|
|
139
|
+
{ day: "Tomorrow", high: tempC + 1, low: tempC - 4, condition: conditions[(hash + 1) % conditions.length] },
|
|
140
|
+
{ day: "Day after", high: tempC + 3, low: tempC - 2, condition: conditions[(hash + 2) % conditions.length] },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Calculator implementation
|
|
145
|
+
function calculate(operation, a, b) {
|
|
146
|
+
let result;
|
|
147
|
+
let expression;
|
|
148
|
+
switch (operation) {
|
|
149
|
+
case "add":
|
|
150
|
+
if (b === undefined)
|
|
151
|
+
throw new Error("Second operand (b) required for addition");
|
|
152
|
+
result = a + b;
|
|
153
|
+
expression = `${a} + ${b}`;
|
|
154
|
+
break;
|
|
155
|
+
case "subtract":
|
|
156
|
+
if (b === undefined)
|
|
157
|
+
throw new Error("Second operand (b) required for subtraction");
|
|
158
|
+
result = a - b;
|
|
159
|
+
expression = `${a} - ${b}`;
|
|
160
|
+
break;
|
|
161
|
+
case "multiply":
|
|
162
|
+
if (b === undefined)
|
|
163
|
+
throw new Error("Second operand (b) required for multiplication");
|
|
164
|
+
result = a * b;
|
|
165
|
+
expression = `${a} * ${b}`;
|
|
166
|
+
break;
|
|
167
|
+
case "divide":
|
|
168
|
+
if (b === undefined)
|
|
169
|
+
throw new Error("Second operand (b) required for division");
|
|
170
|
+
if (b === 0)
|
|
171
|
+
throw new Error("Cannot divide by zero");
|
|
172
|
+
result = a / b;
|
|
173
|
+
expression = `${a} / ${b}`;
|
|
174
|
+
break;
|
|
175
|
+
case "power":
|
|
176
|
+
if (b === undefined)
|
|
177
|
+
throw new Error("Second operand (b) required for power operation");
|
|
178
|
+
result = Math.pow(a, b);
|
|
179
|
+
expression = `${a} ^ ${b}`;
|
|
180
|
+
break;
|
|
181
|
+
case "sqrt":
|
|
182
|
+
if (a < 0)
|
|
183
|
+
throw new Error("Cannot calculate square root of negative number");
|
|
184
|
+
result = Math.sqrt(a);
|
|
185
|
+
expression = `sqrt(${a})`;
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
operation,
|
|
192
|
+
expression,
|
|
193
|
+
operands: { a, b },
|
|
194
|
+
result,
|
|
195
|
+
resultType: Number.isInteger(result) ? "integer" : "float",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// Database operations
|
|
199
|
+
function databaseQuery(action, id, query, limit) {
|
|
200
|
+
switch (action) {
|
|
201
|
+
case "get": {
|
|
202
|
+
if (!id) {
|
|
203
|
+
throw new Error("ID required for get action");
|
|
204
|
+
}
|
|
205
|
+
const record = simulatedDatabase.get(id);
|
|
206
|
+
if (!record) {
|
|
207
|
+
throw new Error(`Record with ID '${id}' not found`);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
action,
|
|
211
|
+
record,
|
|
212
|
+
found: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
case "list": {
|
|
216
|
+
const records = Array.from(simulatedDatabase.values()).slice(0, limit ?? 10);
|
|
217
|
+
return {
|
|
218
|
+
action,
|
|
219
|
+
records,
|
|
220
|
+
count: records.length,
|
|
221
|
+
total: simulatedDatabase.size,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
case "search": {
|
|
225
|
+
if (!query) {
|
|
226
|
+
throw new Error("Query required for search action");
|
|
227
|
+
}
|
|
228
|
+
const queryLower = query.toLowerCase();
|
|
229
|
+
const records = Array.from(simulatedDatabase.values())
|
|
230
|
+
.filter((r) => r.name.toLowerCase().includes(queryLower))
|
|
231
|
+
.slice(0, limit ?? 10);
|
|
232
|
+
return {
|
|
233
|
+
action,
|
|
234
|
+
query,
|
|
235
|
+
records,
|
|
236
|
+
count: records.length,
|
|
237
|
+
total: simulatedDatabase.size,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
throw new Error(`Unknown action: ${action}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Create MCP server
|
|
245
|
+
const server = new Server({
|
|
246
|
+
name: "lafs-mcp-server",
|
|
247
|
+
version: "1.0.0",
|
|
248
|
+
}, {
|
|
249
|
+
capabilities: {
|
|
250
|
+
tools: {},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
// Handle tool listing
|
|
254
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
255
|
+
return {
|
|
256
|
+
tools: TOOLS,
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
// Handle tool calls
|
|
260
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
261
|
+
const { name, arguments: args } = request.params;
|
|
262
|
+
const budget = typeof args?._budget === "number" ? args._budget : undefined;
|
|
263
|
+
try {
|
|
264
|
+
let result;
|
|
265
|
+
switch (name) {
|
|
266
|
+
case "weather": {
|
|
267
|
+
const location = String(args?.location ?? "");
|
|
268
|
+
const units = String(args?.units ?? "celsius");
|
|
269
|
+
if (!location) {
|
|
270
|
+
throw new Error("Location is required");
|
|
271
|
+
}
|
|
272
|
+
result = await getWeather(location, units);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "calculator": {
|
|
276
|
+
const operation = String(args?.operation ?? "");
|
|
277
|
+
const a = Number(args?.a);
|
|
278
|
+
const b = args?.b !== undefined ? Number(args?.b) : undefined;
|
|
279
|
+
if (!operation || Number.isNaN(a)) {
|
|
280
|
+
throw new Error("Operation and operand 'a' are required");
|
|
281
|
+
}
|
|
282
|
+
result = calculate(operation, a, b);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "database_query": {
|
|
286
|
+
const action = String(args?.action ?? "");
|
|
287
|
+
const id = args?.id !== undefined ? String(args?.id) : undefined;
|
|
288
|
+
const query = args?.query !== undefined ? String(args?.query) : undefined;
|
|
289
|
+
const limit = args?.limit !== undefined ? Number(args?.limit) : undefined;
|
|
290
|
+
if (!action) {
|
|
291
|
+
throw new Error("Action is required");
|
|
292
|
+
}
|
|
293
|
+
result = databaseQuery(action, id, query, limit);
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
default:
|
|
297
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
298
|
+
}
|
|
299
|
+
// Create MCP result
|
|
300
|
+
const mcpResult = {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: JSON.stringify(result, null, 2),
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
isError: false,
|
|
308
|
+
};
|
|
309
|
+
// Wrap in LAFS envelope
|
|
310
|
+
const envelope = wrapMCPResult(mcpResult, `tools/${name}`, budget);
|
|
311
|
+
// Return the LAFS envelope as text content
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: JSON.stringify(envelope),
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// Create error MCP result
|
|
323
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
324
|
+
const mcpResult = {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: errorMessage,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
isError: true,
|
|
332
|
+
};
|
|
333
|
+
// Wrap in LAFS error envelope
|
|
334
|
+
const envelope = wrapMCPResult(mcpResult, `tools/${name}`, budget);
|
|
335
|
+
return {
|
|
336
|
+
content: [
|
|
337
|
+
{
|
|
338
|
+
type: "text",
|
|
339
|
+
text: JSON.stringify(envelope),
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
isError: true,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
// Start server
|
|
347
|
+
async function main() {
|
|
348
|
+
const transport = new StdioServerTransport();
|
|
349
|
+
console.error("LAFS-MCP Server starting...");
|
|
350
|
+
console.error("Available tools: weather, calculator, database_query");
|
|
351
|
+
console.error("All responses are wrapped in LAFS-compliant envelopes");
|
|
352
|
+
await server.connect(transport);
|
|
353
|
+
console.error("LAFS-MCP Server running on stdio");
|
|
354
|
+
}
|
|
355
|
+
main().catch((error) => {
|
|
356
|
+
console.error("Fatal error:", error);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
});
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS A2A Bridge
|
|
3
|
+
*
|
|
4
|
+
* Integration with official @a2a-js/sdk for Agent-to-Agent communication.
|
|
5
|
+
* LAFS provides envelope wrapping and token budget support.
|
|
6
|
+
*/
|
|
7
|
+
import { A2AClient } from '@a2a-js/sdk/client';
|
|
8
|
+
import { Artifact, Part, SendMessageResponse, JSONRPCErrorResponse } from '@a2a-js/sdk';
|
|
9
|
+
export interface LafsA2AConfig {
|
|
10
|
+
defaultBudget?: {
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
maxItems?: number;
|
|
13
|
+
};
|
|
14
|
+
envelopeResponses?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface LafsEnvelope {
|
|
17
|
+
$schema: string;
|
|
18
|
+
_meta: {
|
|
19
|
+
specVersion: string;
|
|
20
|
+
operation: string;
|
|
21
|
+
requestId: string;
|
|
22
|
+
mvi: string;
|
|
23
|
+
_tokenEstimate?: {
|
|
24
|
+
estimated: number;
|
|
25
|
+
budget?: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
success: boolean;
|
|
29
|
+
result: unknown;
|
|
30
|
+
error: null | {
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
category: string;
|
|
34
|
+
retryable: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wrap A2A client with LAFS envelope support
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
43
|
+
* import { withLafsEnvelope } from '@lafs/envelope/a2a';
|
|
44
|
+
*
|
|
45
|
+
* const factory = new ClientFactory();
|
|
46
|
+
* const a2aClient = await factory.createFromUrl('http://localhost:4000');
|
|
47
|
+
*
|
|
48
|
+
* const client = withLafsEnvelope(a2aClient, {
|
|
49
|
+
* envelopeResponses: true,
|
|
50
|
+
* defaultBudget: { maxTokens: 4000 }
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* const result = await client.sendMessage({
|
|
54
|
+
* message: { role: 'user', parts: [{ text: 'Hello' }] }
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Access LAFS envelope
|
|
58
|
+
* const envelope = result.getLafsEnvelope();
|
|
59
|
+
* console.log(envelope._meta._tokenEstimate);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function withLafsEnvelope(client: A2AClient, config?: LafsA2AConfig): LafsA2AClient;
|
|
63
|
+
export declare class LafsA2AClient {
|
|
64
|
+
private client;
|
|
65
|
+
private config;
|
|
66
|
+
constructor(client: A2AClient, config: LafsA2AConfig);
|
|
67
|
+
sendMessage(params: {
|
|
68
|
+
message: {
|
|
69
|
+
role: 'user' | 'agent';
|
|
70
|
+
parts: Part[];
|
|
71
|
+
};
|
|
72
|
+
budget?: {
|
|
73
|
+
maxTokens?: number;
|
|
74
|
+
maxItems?: number;
|
|
75
|
+
};
|
|
76
|
+
}): Promise<LafsA2AResult>;
|
|
77
|
+
private generateId;
|
|
78
|
+
}
|
|
79
|
+
export declare class LafsA2AResult {
|
|
80
|
+
private result;
|
|
81
|
+
private budget;
|
|
82
|
+
constructor(result: SendMessageResponse, budget: {
|
|
83
|
+
maxTokens?: number;
|
|
84
|
+
maxItems?: number;
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Get the underlying A2A result
|
|
88
|
+
*/
|
|
89
|
+
getA2AResult(): SendMessageResponse;
|
|
90
|
+
/**
|
|
91
|
+
* Check if result is an error
|
|
92
|
+
*/
|
|
93
|
+
isError(): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Get error details if result is an error
|
|
96
|
+
*/
|
|
97
|
+
getError(): JSONRPCErrorResponse | null;
|
|
98
|
+
/**
|
|
99
|
+
* Extract LAFS envelope from A2A artifact
|
|
100
|
+
*/
|
|
101
|
+
getLafsEnvelope(): LafsEnvelope | null;
|
|
102
|
+
/**
|
|
103
|
+
* Check if result contains LAFS envelope
|
|
104
|
+
*/
|
|
105
|
+
hasLafsEnvelope(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Get token estimate from envelope
|
|
108
|
+
*/
|
|
109
|
+
getTokenEstimate(): {
|
|
110
|
+
estimated: number;
|
|
111
|
+
budget?: number;
|
|
112
|
+
} | null;
|
|
113
|
+
private isLafsEnvelope;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a LAFS artifact for A2A
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const artifact = createLafsArtifact({
|
|
121
|
+
* success: true,
|
|
122
|
+
* result: { data: '...' },
|
|
123
|
+
* meta: { operation: 'analysis.run' }
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* task.artifacts.push(artifact);
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function createLafsArtifact(envelope: LafsEnvelope): Artifact;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS A2A Bridge
|
|
3
|
+
*
|
|
4
|
+
* Integration with official @a2a-js/sdk for Agent-to-Agent communication.
|
|
5
|
+
* LAFS provides envelope wrapping and token budget support.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Wrap A2A client with LAFS envelope support
|
|
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
|
+
* ```
|
|
31
|
+
*/
|
|
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
|
+
export class LafsA2AResult {
|
|
69
|
+
result;
|
|
70
|
+
budget;
|
|
71
|
+
constructor(result, budget) {
|
|
72
|
+
this.result = result;
|
|
73
|
+
this.budget = budget;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the underlying A2A result
|
|
77
|
+
*/
|
|
78
|
+
getA2AResult() {
|
|
79
|
+
return this.result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if result is an error
|
|
83
|
+
*/
|
|
84
|
+
isError() {
|
|
85
|
+
return 'error' in this.result;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get error details if result is an error
|
|
89
|
+
*/
|
|
90
|
+
getError() {
|
|
91
|
+
if (this.isError()) {
|
|
92
|
+
return this.result;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extract LAFS envelope from A2A artifact
|
|
98
|
+
*/
|
|
99
|
+
getLafsEnvelope() {
|
|
100
|
+
if (this.isError()) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const successResult = this.result;
|
|
104
|
+
// Check if result is a Task
|
|
105
|
+
if (successResult.result?.kind !== 'task') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const task = successResult.result;
|
|
109
|
+
if (!task.artifacts || task.artifacts.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Find LAFS envelope in artifacts
|
|
113
|
+
for (const artifact of task.artifacts) {
|
|
114
|
+
for (const part of artifact.parts) {
|
|
115
|
+
if (part.kind === 'data' && this.isLafsEnvelope(part.data)) {
|
|
116
|
+
return part.data;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if result contains LAFS envelope
|
|
124
|
+
*/
|
|
125
|
+
hasLafsEnvelope() {
|
|
126
|
+
return this.getLafsEnvelope() !== null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get token estimate from envelope
|
|
130
|
+
*/
|
|
131
|
+
getTokenEstimate() {
|
|
132
|
+
const envelope = this.getLafsEnvelope();
|
|
133
|
+
return envelope?._meta?._tokenEstimate ?? null;
|
|
134
|
+
}
|
|
135
|
+
isLafsEnvelope(data) {
|
|
136
|
+
return (typeof data === 'object' &&
|
|
137
|
+
data !== null &&
|
|
138
|
+
'$schema' in data &&
|
|
139
|
+
'_meta' in data &&
|
|
140
|
+
'success' in data);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Create a LAFS artifact for A2A
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const artifact = createLafsArtifact({
|
|
149
|
+
* success: true,
|
|
150
|
+
* result: { data: '...' },
|
|
151
|
+
* meta: { operation: 'analysis.run' }
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* task.artifacts.push(artifact);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createLafsArtifact(envelope) {
|
|
158
|
+
return {
|
|
159
|
+
artifactId: generateId(),
|
|
160
|
+
name: 'lafs_response',
|
|
161
|
+
parts: [{
|
|
162
|
+
kind: 'data',
|
|
163
|
+
data: envelope
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function generateId() {
|
|
168
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
169
|
+
const r = Math.random() * 16 | 0;
|
|
170
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
171
|
+
return v.toString(16);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Agent-to-Agent (A2A) Integration
|
|
3
|
+
*
|
|
4
|
+
* This module provides integration between LAFS and the official
|
|
5
|
+
* @a2a-js/sdk for Agent-to-Agent communication.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
10
|
+
* import { withLafsEnvelope } from '@cleocode/lafs-protocol/a2a';
|
|
11
|
+
*
|
|
12
|
+
* // Create official A2A client
|
|
13
|
+
* const factory = new ClientFactory();
|
|
14
|
+
* const a2aClient = await factory.createFromUrl('http://agent.example.com');
|
|
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
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Extract LAFS envelope from response
|
|
30
|
+
* const envelope = result.getLafsEnvelope();
|
|
31
|
+
* if (envelope) {
|
|
32
|
+
* console.log(envelope._meta._tokenEstimate);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export { withLafsEnvelope, LafsA2AClient, LafsA2AResult, createLafsArtifact, type LafsA2AConfig, type LafsEnvelope } from './bridge.js';
|