@cleocode/lafs-protocol 0.1.1 → 1.0.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/README.md +129 -23
- 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 +103 -14
- package/dist/schemas/v1/error-registry.json +19 -1
- package/dist/src/budgetEnforcement.d.ts +84 -0
- package/dist/src/budgetEnforcement.js +328 -0
- package/dist/src/cli.d.ts +14 -0
- package/dist/src/cli.js +14 -0
- package/dist/src/conformance.js +80 -7
- package/dist/src/discovery.d.ts +127 -0
- package/dist/src/discovery.js +304 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/mcpAdapter.d.ts +28 -0
- package/dist/src/mcpAdapter.js +281 -0
- package/dist/src/tokenEstimator.d.ts +87 -0
- package/dist/src/tokenEstimator.js +238 -0
- package/dist/src/types.d.ts +67 -7
- package/lafs.md +331 -23
- package/package.json +8 -3
- package/schemas/v1/context-ledger.schema.json +70 -0
- package/schemas/v1/discovery.schema.json +132 -0
- package/schemas/v1/envelope.schema.json +103 -14
- package/schemas/v1/error-registry.json +19 -1
|
@@ -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
|
+
});
|
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
"$id": "https://lafs.dev/schemas/v1/envelope.schema.json",
|
|
4
4
|
"title": "LAFS Envelope v1",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"additionalProperties": false,
|
|
7
6
|
"required": [
|
|
8
7
|
"$schema",
|
|
9
8
|
"_meta",
|
|
10
9
|
"success",
|
|
11
|
-
"result"
|
|
12
|
-
"error",
|
|
13
|
-
"page"
|
|
10
|
+
"result"
|
|
14
11
|
],
|
|
15
12
|
"properties": {
|
|
16
13
|
"$schema": {
|
|
@@ -62,11 +59,28 @@
|
|
|
62
59
|
"type": "boolean"
|
|
63
60
|
},
|
|
64
61
|
"mvi": {
|
|
65
|
-
"type": "
|
|
62
|
+
"type": "string",
|
|
63
|
+
"enum": ["minimal", "standard", "full", "custom"],
|
|
64
|
+
"description": "Disclosure level: minimal (MVI only), standard (common fields), full (all fields), custom (per _fields parameter)"
|
|
66
65
|
},
|
|
67
66
|
"contextVersion": {
|
|
68
67
|
"type": "integer",
|
|
69
68
|
"minimum": 0
|
|
69
|
+
},
|
|
70
|
+
"warnings": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"code": { "type": "string", "description": "Warning identifier (e.g., DEPRECATED_FIELD)" },
|
|
76
|
+
"message": { "type": "string", "description": "Human-readable warning" },
|
|
77
|
+
"deprecated": { "type": "string", "description": "The deprecated feature or field name" },
|
|
78
|
+
"replacement": { "type": "string", "description": "Suggested replacement, if any" },
|
|
79
|
+
"removeBy": { "type": "string", "description": "Version when the deprecated feature will be removed" }
|
|
80
|
+
},
|
|
81
|
+
"required": ["code", "message"]
|
|
82
|
+
},
|
|
83
|
+
"description": "Non-fatal advisory warnings (deprecations, migration hints)"
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
},
|
|
@@ -127,14 +141,7 @@
|
|
|
127
141
|
"page": {
|
|
128
142
|
"type": ["object", "null"],
|
|
129
143
|
"additionalProperties": false,
|
|
130
|
-
"required": [
|
|
131
|
-
"mode",
|
|
132
|
-
"limit",
|
|
133
|
-
"offset",
|
|
134
|
-
"nextCursor",
|
|
135
|
-
"hasMore",
|
|
136
|
-
"total"
|
|
137
|
-
],
|
|
144
|
+
"required": ["mode"],
|
|
138
145
|
"properties": {
|
|
139
146
|
"mode": {
|
|
140
147
|
"type": "string",
|
|
@@ -160,7 +167,61 @@
|
|
|
160
167
|
"type": ["integer", "null"],
|
|
161
168
|
"minimum": 0
|
|
162
169
|
}
|
|
163
|
-
}
|
|
170
|
+
},
|
|
171
|
+
"allOf": [
|
|
172
|
+
{
|
|
173
|
+
"if": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": { "mode": { "const": "cursor" } },
|
|
176
|
+
"required": ["mode"]
|
|
177
|
+
},
|
|
178
|
+
"then": {
|
|
179
|
+
"required": ["mode", "nextCursor", "hasMore"],
|
|
180
|
+
"properties": {
|
|
181
|
+
"mode": true,
|
|
182
|
+
"nextCursor": true,
|
|
183
|
+
"hasMore": true,
|
|
184
|
+
"limit": true,
|
|
185
|
+
"total": true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"if": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"properties": { "mode": { "const": "offset" } },
|
|
193
|
+
"required": ["mode"]
|
|
194
|
+
},
|
|
195
|
+
"then": {
|
|
196
|
+
"required": ["mode", "limit", "offset", "hasMore"],
|
|
197
|
+
"properties": {
|
|
198
|
+
"mode": true,
|
|
199
|
+
"limit": true,
|
|
200
|
+
"offset": true,
|
|
201
|
+
"hasMore": true,
|
|
202
|
+
"total": true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"if": {
|
|
208
|
+
"type": "object",
|
|
209
|
+
"properties": { "mode": { "const": "none" } },
|
|
210
|
+
"required": ["mode"]
|
|
211
|
+
},
|
|
212
|
+
"then": {
|
|
213
|
+
"required": ["mode"],
|
|
214
|
+
"properties": {
|
|
215
|
+
"mode": true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
"_extensions": {
|
|
222
|
+
"type": "object",
|
|
223
|
+
"description": "Vendor extensions. Keys SHOULD use x- prefix (e.g., x-myvendor-trace-id).",
|
|
224
|
+
"additionalProperties": true
|
|
164
225
|
}
|
|
165
226
|
},
|
|
166
227
|
"allOf": [
|
|
@@ -183,11 +244,39 @@
|
|
|
183
244
|
}
|
|
184
245
|
},
|
|
185
246
|
"then": {
|
|
247
|
+
"required": ["error"],
|
|
186
248
|
"properties": {
|
|
187
249
|
"result": { "type": "null" },
|
|
188
250
|
"error": { "type": "object" }
|
|
189
251
|
}
|
|
190
252
|
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"if": {
|
|
256
|
+
"properties": {
|
|
257
|
+
"_meta": {
|
|
258
|
+
"type": "object",
|
|
259
|
+
"properties": {
|
|
260
|
+
"strict": { "const": true }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"then": {
|
|
266
|
+
"additionalProperties": false,
|
|
267
|
+
"properties": {
|
|
268
|
+
"$schema": true,
|
|
269
|
+
"_meta": true,
|
|
270
|
+
"success": true,
|
|
271
|
+
"result": true,
|
|
272
|
+
"error": true,
|
|
273
|
+
"page": true,
|
|
274
|
+
"_extensions": true
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
"else": {
|
|
278
|
+
"additionalProperties": true
|
|
279
|
+
}
|
|
191
280
|
}
|
|
192
281
|
]
|
|
193
282
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"codes": [
|
|
5
5
|
{
|
|
@@ -91,6 +91,24 @@
|
|
|
91
91
|
"httpStatus": 426,
|
|
92
92
|
"grpcStatus": "FAILED_PRECONDITION",
|
|
93
93
|
"cliExit": 10
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"code": "E_DISCLOSURE_UNKNOWN_FIELD",
|
|
97
|
+
"category": "VALIDATION",
|
|
98
|
+
"description": "Unrecognized expansion field in _expand parameter",
|
|
99
|
+
"retryable": false,
|
|
100
|
+
"httpStatus": 400,
|
|
101
|
+
"grpcStatus": "INVALID_ARGUMENT",
|
|
102
|
+
"cliExit": 2
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"code": "E_MVI_BUDGET_EXCEEDED",
|
|
106
|
+
"category": "VALIDATION",
|
|
107
|
+
"description": "Response exceeds declared MVI budget (token, byte, or item limit)",
|
|
108
|
+
"retryable": false,
|
|
109
|
+
"httpStatus": 413,
|
|
110
|
+
"grpcStatus": "RESOURCE_EXHAUSTED",
|
|
111
|
+
"cliExit": 2
|
|
94
112
|
}
|
|
95
113
|
]
|
|
96
114
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Budget Enforcement
|
|
3
|
+
*
|
|
4
|
+
* Middleware for enforcing MVI (Minimal Viable Interface) token budgets on LAFS envelopes.
|
|
5
|
+
* Provides budget checking, truncation, and error generation for exceeded budgets.
|
|
6
|
+
*/
|
|
7
|
+
import type { LAFSEnvelope } from "./types.js";
|
|
8
|
+
import type { BudgetEnforcementOptions, TokenEstimate, BudgetEnforcementResult } from "./types.js";
|
|
9
|
+
import { TokenEstimator } from "./tokenEstimator.js";
|
|
10
|
+
/**
|
|
11
|
+
* Budget exceeded error code from LAFS error registry
|
|
12
|
+
*/
|
|
13
|
+
declare const BUDGET_EXCEEDED_CODE = "E_MVI_BUDGET_EXCEEDED";
|
|
14
|
+
/**
|
|
15
|
+
* Apply budget enforcement to an envelope.
|
|
16
|
+
*
|
|
17
|
+
* @param envelope - The LAFS envelope to check
|
|
18
|
+
* @param budget - Maximum allowed tokens
|
|
19
|
+
* @param options - Budget enforcement options
|
|
20
|
+
* @returns Enforce result with potentially modified envelope
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyBudgetEnforcement(envelope: LAFSEnvelope, budget: number, options?: BudgetEnforcementOptions): BudgetEnforcementResult;
|
|
23
|
+
/**
|
|
24
|
+
* Type for middleware function
|
|
25
|
+
*/
|
|
26
|
+
type EnvelopeMiddleware = (envelope: LAFSEnvelope, next: () => LAFSEnvelope | Promise<LAFSEnvelope>) => Promise<LAFSEnvelope> | LAFSEnvelope;
|
|
27
|
+
/**
|
|
28
|
+
* Create a budget enforcement middleware function.
|
|
29
|
+
*
|
|
30
|
+
* @param budget - Maximum allowed tokens for response
|
|
31
|
+
* @param options - Budget enforcement options
|
|
32
|
+
* @returns Middleware function that enforces budget
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const middleware = withBudget(1000, { truncateOnExceed: true });
|
|
37
|
+
* const result = await middleware(envelope, async () => nextEnvelope);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function withBudget(budget: number, options?: BudgetEnforcementOptions): EnvelopeMiddleware;
|
|
41
|
+
/**
|
|
42
|
+
* Check if an envelope has exceeded its budget without modifying it.
|
|
43
|
+
*
|
|
44
|
+
* @param envelope - The LAFS envelope to check
|
|
45
|
+
* @param budget - Maximum allowed tokens
|
|
46
|
+
* @returns Budget check result
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkBudget(envelope: LAFSEnvelope, budget: number): {
|
|
49
|
+
exceeded: boolean;
|
|
50
|
+
estimated: number;
|
|
51
|
+
remaining: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Synchronous version of withBudget for non-async contexts.
|
|
55
|
+
*
|
|
56
|
+
* @param budget - Maximum allowed tokens for response
|
|
57
|
+
* @param options - Budget enforcement options
|
|
58
|
+
* @returns Middleware function that enforces budget synchronously
|
|
59
|
+
*/
|
|
60
|
+
export declare function withBudgetSync(budget: number, options?: BudgetEnforcementOptions): (envelope: LAFSEnvelope, next: () => LAFSEnvelope) => LAFSEnvelope;
|
|
61
|
+
/**
|
|
62
|
+
* Higher-order function that wraps a handler with budget enforcement.
|
|
63
|
+
*
|
|
64
|
+
* @param handler - The handler function to wrap
|
|
65
|
+
* @param budget - Maximum allowed tokens
|
|
66
|
+
* @param options - Budget enforcement options
|
|
67
|
+
* @returns Wrapped handler with budget enforcement
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const myHandler = async (request: Request) => ({ success: true, result: { data } });
|
|
72
|
+
* const budgetedHandler = wrapWithBudget(myHandler, 1000, { truncateOnExceed: true });
|
|
73
|
+
* const result = await budgetedHandler(request);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function wrapWithBudget<TArgs extends unknown[], TResult extends LAFSEnvelope>(handler: (...args: TArgs) => TResult | Promise<TResult>, budget: number, options?: BudgetEnforcementOptions): (...args: TArgs) => Promise<LAFSEnvelope>;
|
|
77
|
+
/**
|
|
78
|
+
* Compose multiple middleware functions into a single middleware.
|
|
79
|
+
* Middleware is executed in order (left to right).
|
|
80
|
+
*/
|
|
81
|
+
export declare function composeMiddleware(...middlewares: EnvelopeMiddleware[]): EnvelopeMiddleware;
|
|
82
|
+
export type { BudgetEnforcementOptions, TokenEstimate, BudgetEnforcementResult };
|
|
83
|
+
export { TokenEstimator };
|
|
84
|
+
export { BUDGET_EXCEEDED_CODE };
|