@cleocode/lafs-protocol 0.5.0 → 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/LICENSE +0 -0
- package/README.md +0 -0
- 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/budgetEnforcement.d.ts +84 -0
- package/dist/src/budgetEnforcement.js +328 -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/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 +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 +164 -0
- package/package.json +8 -3
- 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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Token Estimator
|
|
3
|
+
*
|
|
4
|
+
* Provides character-based token estimation for LAFS envelopes and JSON payloads.
|
|
5
|
+
* Uses the approximation: 1 token ≈ 4 characters.
|
|
6
|
+
* Properly handles nested objects, arrays, Unicode graphemes, and circular references.
|
|
7
|
+
*/
|
|
8
|
+
export interface TokenEstimatorOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Characters per token ratio (default: 4)
|
|
11
|
+
*/
|
|
12
|
+
charsPerToken?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum depth to traverse for circular reference detection (default: 100)
|
|
15
|
+
*/
|
|
16
|
+
maxDepth?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Maximum string length to process for Unicode grapheme counting (default: 100000)
|
|
19
|
+
*/
|
|
20
|
+
maxStringLength?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* TokenEstimator provides character-based token counting for JSON payloads.
|
|
24
|
+
*
|
|
25
|
+
* Algorithm:
|
|
26
|
+
* 1. Serialize value to JSON (handling circular refs)
|
|
27
|
+
* 2. Count Unicode graphemes (not bytes)
|
|
28
|
+
* 3. Divide by charsPerToken ratio (default 4)
|
|
29
|
+
* 4. Add overhead for structural characters
|
|
30
|
+
*/
|
|
31
|
+
export declare class TokenEstimator {
|
|
32
|
+
private options;
|
|
33
|
+
constructor(options?: TokenEstimatorOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Estimate tokens for any JavaScript value.
|
|
36
|
+
* Handles circular references, nested objects, arrays, and Unicode.
|
|
37
|
+
*
|
|
38
|
+
* @param value - Any value to estimate
|
|
39
|
+
* @returns Estimated token count
|
|
40
|
+
*/
|
|
41
|
+
estimate(value: unknown): number;
|
|
42
|
+
/**
|
|
43
|
+
* Estimate tokens from a JSON string.
|
|
44
|
+
* More efficient if you already have the JSON string.
|
|
45
|
+
*
|
|
46
|
+
* @param json - JSON string to estimate
|
|
47
|
+
* @returns Estimated token count
|
|
48
|
+
*/
|
|
49
|
+
estimateJSON(json: string): number;
|
|
50
|
+
/**
|
|
51
|
+
* Internal recursive estimation with circular reference tracking.
|
|
52
|
+
*/
|
|
53
|
+
private estimateWithTracking;
|
|
54
|
+
/**
|
|
55
|
+
* Estimate tokens for an array.
|
|
56
|
+
*/
|
|
57
|
+
private estimateArray;
|
|
58
|
+
/**
|
|
59
|
+
* Estimate tokens for a plain object.
|
|
60
|
+
*/
|
|
61
|
+
private estimateObject;
|
|
62
|
+
/**
|
|
63
|
+
* Check if a value can be safely serialized (no circular refs).
|
|
64
|
+
*/
|
|
65
|
+
canSerialize(value: unknown): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Serialize value to JSON with circular reference handling.
|
|
68
|
+
* Circular refs are replaced with "[Circular]".
|
|
69
|
+
*/
|
|
70
|
+
safeStringify(value: unknown): string;
|
|
71
|
+
/**
|
|
72
|
+
* Create a safe copy of a value with circular refs removed.
|
|
73
|
+
*/
|
|
74
|
+
safeCopy<T>(value: T): T;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Global token estimator instance with default settings.
|
|
78
|
+
*/
|
|
79
|
+
export declare const defaultEstimator: TokenEstimator;
|
|
80
|
+
/**
|
|
81
|
+
* Convenience function to estimate tokens for a value.
|
|
82
|
+
*/
|
|
83
|
+
export declare function estimateTokens(value: unknown, options?: TokenEstimatorOptions): number;
|
|
84
|
+
/**
|
|
85
|
+
* Convenience function to estimate tokens from a JSON string.
|
|
86
|
+
*/
|
|
87
|
+
export declare function estimateTokensJSON(json: string, options?: TokenEstimatorOptions): number;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Token Estimator
|
|
3
|
+
*
|
|
4
|
+
* Provides character-based token estimation for LAFS envelopes and JSON payloads.
|
|
5
|
+
* Uses the approximation: 1 token ≈ 4 characters.
|
|
6
|
+
* Properly handles nested objects, arrays, Unicode graphemes, and circular references.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Counts Unicode graphemes in a string using Intl.Segmenter when available.
|
|
10
|
+
* Falls back to character counting for environments without Intl.Segmenter.
|
|
11
|
+
*/
|
|
12
|
+
function countGraphemes(str) {
|
|
13
|
+
// Use Intl.Segmenter for proper grapheme counting (Node.js 16+, modern browsers)
|
|
14
|
+
if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
|
|
15
|
+
// @ts-ignore - Intl.Segmenter may not be in all TypeScript lib versions
|
|
16
|
+
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
return Array.from(segmenter.segment(str)).length;
|
|
19
|
+
}
|
|
20
|
+
// Fallback: count code points using spread operator (handles surrogate pairs)
|
|
21
|
+
return [...str].length;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Default options for token estimation
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_OPTIONS = {
|
|
27
|
+
charsPerToken: 4,
|
|
28
|
+
maxDepth: 100,
|
|
29
|
+
maxStringLength: 100000,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* TokenEstimator provides character-based token counting for JSON payloads.
|
|
33
|
+
*
|
|
34
|
+
* Algorithm:
|
|
35
|
+
* 1. Serialize value to JSON (handling circular refs)
|
|
36
|
+
* 2. Count Unicode graphemes (not bytes)
|
|
37
|
+
* 3. Divide by charsPerToken ratio (default 4)
|
|
38
|
+
* 4. Add overhead for structural characters
|
|
39
|
+
*/
|
|
40
|
+
export class TokenEstimator {
|
|
41
|
+
options;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Estimate tokens for any JavaScript value.
|
|
47
|
+
* Handles circular references, nested objects, arrays, and Unicode.
|
|
48
|
+
*
|
|
49
|
+
* @param value - Any value to estimate
|
|
50
|
+
* @returns Estimated token count
|
|
51
|
+
*/
|
|
52
|
+
estimate(value) {
|
|
53
|
+
return this.estimateWithTracking(value, new WeakSet(), 0);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Estimate tokens from a JSON string.
|
|
57
|
+
* More efficient if you already have the JSON string.
|
|
58
|
+
*
|
|
59
|
+
* @param json - JSON string to estimate
|
|
60
|
+
* @returns Estimated token count
|
|
61
|
+
*/
|
|
62
|
+
estimateJSON(json) {
|
|
63
|
+
// Count graphemes in the JSON string
|
|
64
|
+
const graphemes = countGraphemes(json);
|
|
65
|
+
// Add overhead for JSON structure (brackets, quotes, colons, etc.)
|
|
66
|
+
const structuralOverhead = Math.ceil(graphemes * 0.1);
|
|
67
|
+
return Math.ceil((graphemes + structuralOverhead) / this.options.charsPerToken);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Internal recursive estimation with circular reference tracking.
|
|
71
|
+
*/
|
|
72
|
+
estimateWithTracking(value, seen, depth) {
|
|
73
|
+
// Prevent infinite recursion
|
|
74
|
+
if (depth > this.options.maxDepth) {
|
|
75
|
+
return 1; // Minimal cost for max depth exceeded
|
|
76
|
+
}
|
|
77
|
+
// Handle null
|
|
78
|
+
if (value === null) {
|
|
79
|
+
return 1; // "null" = 4 chars / 4 = 1 token
|
|
80
|
+
}
|
|
81
|
+
// Handle undefined
|
|
82
|
+
if (value === undefined) {
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
// Handle primitives
|
|
86
|
+
const type = typeof value;
|
|
87
|
+
if (type === 'boolean') {
|
|
88
|
+
return value ? 1 : 1; // "true" or "false" ≈ 1 token
|
|
89
|
+
}
|
|
90
|
+
if (type === 'number') {
|
|
91
|
+
const str = String(value);
|
|
92
|
+
return Math.ceil(countGraphemes(str) / this.options.charsPerToken);
|
|
93
|
+
}
|
|
94
|
+
if (type === 'string') {
|
|
95
|
+
const str = value;
|
|
96
|
+
// Limit string length to prevent performance issues
|
|
97
|
+
const truncated = str.length > this.options.maxStringLength
|
|
98
|
+
? str.slice(0, this.options.maxStringLength) + '…'
|
|
99
|
+
: str;
|
|
100
|
+
const graphemes = countGraphemes(truncated);
|
|
101
|
+
// Add 2 for quotes
|
|
102
|
+
return Math.ceil((graphemes + 2) / this.options.charsPerToken);
|
|
103
|
+
}
|
|
104
|
+
// Handle objects and arrays
|
|
105
|
+
if (type === 'object') {
|
|
106
|
+
const obj = value;
|
|
107
|
+
// Check for circular reference
|
|
108
|
+
if (seen.has(obj)) {
|
|
109
|
+
return 1; // Minimal cost for circular ref placeholder
|
|
110
|
+
}
|
|
111
|
+
seen.add(obj);
|
|
112
|
+
try {
|
|
113
|
+
if (Array.isArray(obj)) {
|
|
114
|
+
return this.estimateArray(obj, seen, depth);
|
|
115
|
+
}
|
|
116
|
+
return this.estimateObject(obj, seen, depth);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
seen.delete(obj);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Handle symbols, functions, etc.
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Estimate tokens for an array.
|
|
127
|
+
*/
|
|
128
|
+
estimateArray(arr, seen, depth) {
|
|
129
|
+
let tokens = 1; // Opening bracket [ (already counted as structural)
|
|
130
|
+
for (let i = 0; i < arr.length; i++) {
|
|
131
|
+
tokens += this.estimateWithTracking(arr[i], seen, depth + 1);
|
|
132
|
+
// Add comma separator (except for last element)
|
|
133
|
+
if (i < arr.length - 1) {
|
|
134
|
+
tokens += 1; // comma + space ≈ 2 chars / 4 = 0.5, round up to 1
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
tokens += 1; // Closing bracket ]
|
|
138
|
+
return tokens;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Estimate tokens for a plain object.
|
|
142
|
+
*/
|
|
143
|
+
estimateObject(obj, seen, depth) {
|
|
144
|
+
let tokens = 1; // Opening brace {
|
|
145
|
+
const keys = Object.keys(obj);
|
|
146
|
+
for (let i = 0; i < keys.length; i++) {
|
|
147
|
+
const key = keys[i];
|
|
148
|
+
const value = obj[key];
|
|
149
|
+
// Estimate key (with quotes)
|
|
150
|
+
tokens += Math.ceil((countGraphemes(key) + 2) / this.options.charsPerToken);
|
|
151
|
+
// Colon separator
|
|
152
|
+
tokens += 1; // " : " ≈ 3 chars / 4 = 0.75, round up to 1
|
|
153
|
+
// Estimate value
|
|
154
|
+
tokens += this.estimateWithTracking(value, seen, depth + 1);
|
|
155
|
+
// Comma separator (except for last property)
|
|
156
|
+
if (i < keys.length - 1) {
|
|
157
|
+
tokens += 1; // comma + space ≈ 2 chars / 4 = 0.5, round up to 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
tokens += 1; // Closing brace }
|
|
161
|
+
return tokens;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if a value can be safely serialized (no circular refs).
|
|
165
|
+
*/
|
|
166
|
+
canSerialize(value) {
|
|
167
|
+
try {
|
|
168
|
+
JSON.stringify(value);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Serialize value to JSON with circular reference handling.
|
|
177
|
+
* Circular refs are replaced with "[Circular]".
|
|
178
|
+
*/
|
|
179
|
+
safeStringify(value) {
|
|
180
|
+
const seen = new WeakSet();
|
|
181
|
+
return JSON.stringify(value, (key, val) => {
|
|
182
|
+
if (typeof val === 'object' && val !== null) {
|
|
183
|
+
if (seen.has(val)) {
|
|
184
|
+
return '[Circular]';
|
|
185
|
+
}
|
|
186
|
+
seen.add(val);
|
|
187
|
+
}
|
|
188
|
+
return val;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create a safe copy of a value with circular refs removed.
|
|
193
|
+
*/
|
|
194
|
+
safeCopy(value) {
|
|
195
|
+
const seen = new WeakSet();
|
|
196
|
+
function clone(val) {
|
|
197
|
+
if (val === null || typeof val !== 'object') {
|
|
198
|
+
return val;
|
|
199
|
+
}
|
|
200
|
+
if (seen.has(val)) {
|
|
201
|
+
return '[Circular]';
|
|
202
|
+
}
|
|
203
|
+
seen.add(val);
|
|
204
|
+
try {
|
|
205
|
+
if (Array.isArray(val)) {
|
|
206
|
+
return val.map(clone);
|
|
207
|
+
}
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const [k, v] of Object.entries(val)) {
|
|
210
|
+
result[k] = clone(v);
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
seen.delete(val);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return clone(value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Global token estimator instance with default settings.
|
|
223
|
+
*/
|
|
224
|
+
export const defaultEstimator = new TokenEstimator();
|
|
225
|
+
/**
|
|
226
|
+
* Convenience function to estimate tokens for a value.
|
|
227
|
+
*/
|
|
228
|
+
export function estimateTokens(value, options) {
|
|
229
|
+
const estimator = options ? new TokenEstimator(options) : defaultEstimator;
|
|
230
|
+
return estimator.estimate(value);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Convenience function to estimate tokens from a JSON string.
|
|
234
|
+
*/
|
|
235
|
+
export function estimateTokensJSON(json, options) {
|
|
236
|
+
const estimator = options ? new TokenEstimator(options) : defaultEstimator;
|
|
237
|
+
return estimator.estimateJSON(json);
|
|
238
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -85,3 +85,28 @@ export interface ConformanceReport {
|
|
|
85
85
|
detail?: string;
|
|
86
86
|
}>;
|
|
87
87
|
}
|
|
88
|
+
export type BudgetEnforcementOptions = {
|
|
89
|
+
truncateOnExceed?: boolean;
|
|
90
|
+
onBudgetExceeded?: (estimated: number, budget: number) => void;
|
|
91
|
+
};
|
|
92
|
+
export interface TokenEstimate {
|
|
93
|
+
estimated: number;
|
|
94
|
+
truncated?: boolean;
|
|
95
|
+
originalEstimate?: number;
|
|
96
|
+
}
|
|
97
|
+
export interface LAFSMetaWithBudget extends LAFSMeta {
|
|
98
|
+
_tokenEstimate?: TokenEstimate;
|
|
99
|
+
}
|
|
100
|
+
export interface LAFSEnvelopeWithBudget extends Omit<LAFSEnvelope, '_meta'> {
|
|
101
|
+
_meta: LAFSMetaWithBudget;
|
|
102
|
+
}
|
|
103
|
+
export type MiddlewareFunction = (envelope: LAFSEnvelope) => LAFSEnvelope | Promise<LAFSEnvelope>;
|
|
104
|
+
export type NextFunction = () => LAFSEnvelope | Promise<LAFSEnvelope>;
|
|
105
|
+
export type BudgetMiddleware = (envelope: LAFSEnvelope, next: NextFunction) => Promise<LAFSEnvelope> | LAFSEnvelope;
|
|
106
|
+
export interface BudgetEnforcementResult {
|
|
107
|
+
envelope: LAFSEnvelope;
|
|
108
|
+
withinBudget: boolean;
|
|
109
|
+
estimatedTokens: number;
|
|
110
|
+
budget: number;
|
|
111
|
+
truncated: boolean;
|
|
112
|
+
}
|
package/dist/src/types.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lafs.md
CHANGED
|
@@ -176,6 +176,56 @@ Rules:
|
|
|
176
176
|
- Decisions affecting output MUST be represented in ledger state.
|
|
177
177
|
- Missing required context for a mutating step MUST fail with structured error.
|
|
178
178
|
|
|
179
|
+
### 8.1 Context Retrieval
|
|
180
|
+
|
|
181
|
+
Agents MAY retrieve context ledger state via `GET /_lafs/context/{ledgerId}` with projection modes.
|
|
182
|
+
|
|
183
|
+
#### 8.1.1 Projection Modes
|
|
184
|
+
|
|
185
|
+
**Full Mode (`mode=full`):**
|
|
186
|
+
Returns complete ledger including all entries.
|
|
187
|
+
- Use for: Initial loads, recovery scenarios
|
|
188
|
+
- Supports: Offset-based pagination
|
|
189
|
+
- Response includes: All ledger fields
|
|
190
|
+
|
|
191
|
+
**Delta Mode (`mode=delta&sinceVersion=N`):**
|
|
192
|
+
Returns only entries added since version N.
|
|
193
|
+
- Use for: Active workflows (efficient sync)
|
|
194
|
+
- Response includes:
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"ledgerId": "ctx_abc123",
|
|
198
|
+
"mode": "delta",
|
|
199
|
+
"fromVersion": 10,
|
|
200
|
+
"toVersion": 15,
|
|
201
|
+
"entries": [/* new entries only */],
|
|
202
|
+
"removedConstraints": [/* constraints no longer active */],
|
|
203
|
+
"checksum": "sha256:..."
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Summary Mode (`mode=summary`):**
|
|
208
|
+
Returns checksum and version for validation.
|
|
209
|
+
- Use for: Quick sync validation
|
|
210
|
+
- Response includes only: `ledgerId`, `version`, `checksum`, `entryCount`
|
|
211
|
+
|
|
212
|
+
#### 8.1.2 Query Parameters
|
|
213
|
+
|
|
214
|
+
| Parameter | Type | Description |
|
|
215
|
+
|-----------|------|-------------|
|
|
216
|
+
| `mode` | enum | `full`, `delta`, `summary` |
|
|
217
|
+
| `sinceVersion` | integer | For delta mode: return entries after this version |
|
|
218
|
+
| `filterByOperation` | string[] | Filter entries by operation name(s) |
|
|
219
|
+
| `limit` | integer | Max entries (1-1000, default 100) |
|
|
220
|
+
| `includeChecksum` | boolean | Include integrity checksum (default true) |
|
|
221
|
+
|
|
222
|
+
#### 8.1.3 Agent Guidance
|
|
223
|
+
|
|
224
|
+
- **Initial load**: Use `mode=full` once
|
|
225
|
+
- **Active workflows**: Use `mode=delta` with last known version
|
|
226
|
+
- **Validation**: Use `mode=summary` to verify sync state
|
|
227
|
+
- **Default recommendation**: `delta` mode for agent-optimal behavior
|
|
228
|
+
|
|
179
229
|
---
|
|
180
230
|
|
|
181
231
|
## 9. MVI and Progressive Disclosure
|
|
@@ -212,6 +262,120 @@ Clients MAY request expanded/nested data via the `_expand` request parameter.
|
|
|
212
262
|
- Pagination mode (offset or cursor) MUST be documented.
|
|
213
263
|
- Mixed pagination modes in one request MUST fail validation.
|
|
214
264
|
|
|
265
|
+
### 9.5 Token Budget Signaling
|
|
266
|
+
|
|
267
|
+
Token budget signaling enables clients to declare resource constraints that servers MUST respect when generating responses. This mechanism prevents context window overflow in LLM-driven workflows.
|
|
268
|
+
|
|
269
|
+
#### 9.5.1 Budget Declaration (`_budget`)
|
|
270
|
+
|
|
271
|
+
Clients MAY declare resource constraints via the `_budget` request parameter:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"_budget": {
|
|
276
|
+
"maxTokens": 4000,
|
|
277
|
+
"maxBytes": 32768,
|
|
278
|
+
"maxItems": 100
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Fields:**
|
|
284
|
+
- `maxTokens` (integer) - Maximum approximate tokens
|
|
285
|
+
- `maxBytes` (integer) - Maximum byte size
|
|
286
|
+
- `maxItems` (integer) - Maximum items in lists
|
|
287
|
+
|
|
288
|
+
**Constraints:**
|
|
289
|
+
- At least one field MUST be present
|
|
290
|
+
- All values MUST be positive integers
|
|
291
|
+
- Servers MAY reject budgets exceeding implementation limits
|
|
292
|
+
|
|
293
|
+
#### 9.5.2 Server Behavior
|
|
294
|
+
|
|
295
|
+
Servers MUST:
|
|
296
|
+
1. Parse `_budget` from incoming requests
|
|
297
|
+
2. Estimate/measure response size
|
|
298
|
+
3. Return response within budget OR fail with `E_MVI_BUDGET_EXCEEDED`
|
|
299
|
+
|
|
300
|
+
Servers MAY truncate responses using:
|
|
301
|
+
- **Depth-first**: Remove deepest nested fields
|
|
302
|
+
- **Field priority**: Remove non-essential fields first
|
|
303
|
+
- **Hybrid**: Combine both strategies
|
|
304
|
+
|
|
305
|
+
When truncation occurs, servers MUST include:
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"_meta": {
|
|
309
|
+
"warnings": [{
|
|
310
|
+
"code": "E_MVI_BUDGET_TRUNCATED",
|
|
311
|
+
"message": "Response truncated to fit token budget"
|
|
312
|
+
}],
|
|
313
|
+
"_tokenEstimate": {
|
|
314
|
+
"estimated": 2847,
|
|
315
|
+
"budget": 4000,
|
|
316
|
+
"method": "character_based"
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### 9.5.3 Error Specification
|
|
323
|
+
|
|
324
|
+
**E_MVI_BUDGET_EXCEEDED:**
|
|
325
|
+
- **Category:** `VALIDATION`
|
|
326
|
+
- **Retryable:** `true`
|
|
327
|
+
- **Details:** `estimatedTokens`, `budget`, `excessTokens`, `constraint`
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"error": {
|
|
332
|
+
"code": "E_MVI_BUDGET_EXCEEDED",
|
|
333
|
+
"message": "Response exceeds declared token budget",
|
|
334
|
+
"category": "VALIDATION",
|
|
335
|
+
"retryable": true,
|
|
336
|
+
"details": {
|
|
337
|
+
"estimatedTokens": 5234,
|
|
338
|
+
"budget": 4000,
|
|
339
|
+
"excessTokens": 1234,
|
|
340
|
+
"constraint": "maxTokens"
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### 9.5.4 Token Estimation Algorithm (Normative)
|
|
347
|
+
|
|
348
|
+
Servers MUST implement this algorithm or equivalent (within +/- 10%):
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
FUNCTION estimate_tokens(value, depth = 0):
|
|
352
|
+
IF depth > 20: RETURN INFINITY
|
|
353
|
+
IF value IS null: RETURN 1
|
|
354
|
+
IF value IS boolean: RETURN 1
|
|
355
|
+
IF value IS number: RETURN max(1, len(stringify(value)) / 4)
|
|
356
|
+
IF value IS string:
|
|
357
|
+
graphemes = count_grapheme_clusters(value)
|
|
358
|
+
RETURN max(1, graphemes / 4.0)
|
|
359
|
+
IF value IS array:
|
|
360
|
+
tokens = 2 // []
|
|
361
|
+
FOR item IN value:
|
|
362
|
+
tokens += estimate_tokens(item, depth + 1) + 1
|
|
363
|
+
RETURN tokens
|
|
364
|
+
IF value IS object:
|
|
365
|
+
tokens = 2 // {}
|
|
366
|
+
FOR key, val IN value:
|
|
367
|
+
tokens += estimate_tokens(key, depth + 1)
|
|
368
|
+
tokens += 2 // : and ,
|
|
369
|
+
tokens += estimate_tokens(val, depth + 1)
|
|
370
|
+
RETURN tokens
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Requirements:**
|
|
374
|
+
- Count grapheme clusters (not bytes) for unicode
|
|
375
|
+
- Enforce max depth of 20
|
|
376
|
+
- Handle circular references
|
|
377
|
+
- Complete within 10ms for 100KB payloads
|
|
378
|
+
|
|
215
379
|
---
|
|
216
380
|
|
|
217
381
|
## 10. Strictness
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/lafs-protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "LLM-Agent-First Specification schemas and conformance tooling",
|
|
@@ -50,13 +50,18 @@
|
|
|
50
50
|
],
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"devDependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
54
|
+
"@types/express": "^5.0.6",
|
|
53
55
|
"@types/node": "^24.3.0",
|
|
56
|
+
"@types/supertest": "^6.0.3",
|
|
57
|
+
"supertest": "^7.2.2",
|
|
54
58
|
"tsx": "^4.20.5",
|
|
55
59
|
"typescript": "^5.9.2",
|
|
56
60
|
"vitest": "^2.1.9"
|
|
57
61
|
},
|
|
58
62
|
"dependencies": {
|
|
59
|
-
"ajv": "^8.
|
|
60
|
-
"ajv-formats": "^3.0.1"
|
|
63
|
+
"ajv": "^8.18.0",
|
|
64
|
+
"ajv-formats": "^3.0.1",
|
|
65
|
+
"express": "^5.2.1"
|
|
61
66
|
}
|
|
62
67
|
}
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://lafs.dev/schemas/v1/discovery.schema.json",
|
|
4
|
+
"title": "LAFS Discovery Document",
|
|
5
|
+
"description": "Schema for LAFS agent discovery documents served at /.well-known/lafs.json",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"$schema",
|
|
9
|
+
"lafs_version",
|
|
10
|
+
"service",
|
|
11
|
+
"capabilities",
|
|
12
|
+
"endpoints"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"$schema": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"format": "uri",
|
|
18
|
+
"description": "URL of the schema this document conforms to"
|
|
19
|
+
},
|
|
20
|
+
"lafs_version": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
23
|
+
"description": "LAFS protocol version (semantic versioning)"
|
|
24
|
+
},
|
|
25
|
+
"service": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "Service identification and metadata",
|
|
28
|
+
"required": [
|
|
29
|
+
"name",
|
|
30
|
+
"version"
|
|
31
|
+
],
|
|
32
|
+
"properties": {
|
|
33
|
+
"name": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1,
|
|
36
|
+
"maxLength": 100,
|
|
37
|
+
"description": "Unique service name (kebab-case recommended)"
|
|
38
|
+
},
|
|
39
|
+
"version": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
42
|
+
"description": "Service version (semantic versioning)"
|
|
43
|
+
},
|
|
44
|
+
"description": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"maxLength": 500,
|
|
47
|
+
"description": "Human-readable service description"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"additionalProperties": false
|
|
51
|
+
},
|
|
52
|
+
"capabilities": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"description": "List of LAFS capabilities this service provides",
|
|
55
|
+
"minItems": 1,
|
|
56
|
+
"items": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": [
|
|
59
|
+
"name",
|
|
60
|
+
"version",
|
|
61
|
+
"operations"
|
|
62
|
+
],
|
|
63
|
+
"properties": {
|
|
64
|
+
"name": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"minLength": 1,
|
|
67
|
+
"maxLength": 100,
|
|
68
|
+
"description": "Capability identifier (kebab-case recommended)"
|
|
69
|
+
},
|
|
70
|
+
"version": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
73
|
+
"description": "Capability version (semantic versioning)"
|
|
74
|
+
},
|
|
75
|
+
"description": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"maxLength": 500,
|
|
78
|
+
"description": "Human-readable capability description"
|
|
79
|
+
},
|
|
80
|
+
"operations": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"description": "List of operations this capability supports",
|
|
83
|
+
"minItems": 1,
|
|
84
|
+
"items": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"minLength": 1,
|
|
87
|
+
"maxLength": 50
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"optional": {
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"default": false,
|
|
93
|
+
"description": "Whether this capability is optional for clients"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"additionalProperties": false
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"endpoints": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"description": "URL endpoints for LAFS operations",
|
|
102
|
+
"required": [
|
|
103
|
+
"envelope",
|
|
104
|
+
"discovery"
|
|
105
|
+
],
|
|
106
|
+
"properties": {
|
|
107
|
+
"envelope": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"minLength": 1,
|
|
110
|
+
"description": "URL for envelope submission (POST)"
|
|
111
|
+
},
|
|
112
|
+
"context": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"minLength": 1,
|
|
115
|
+
"description": "URL for context ledger operations"
|
|
116
|
+
},
|
|
117
|
+
"discovery": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"minLength": 1,
|
|
120
|
+
"description": "URL of this discovery document"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"additionalProperties": false
|
|
124
|
+
},
|
|
125
|
+
"extensions": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"description": "Extension fields for vendor-specific metadata",
|
|
128
|
+
"additionalProperties": true
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"additionalProperties": false
|
|
132
|
+
}
|