@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.
Files changed (48) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +7 -3
  3. package/dist/examples/discovery-server.d.ts +8 -0
  4. package/dist/examples/discovery-server.js +216 -0
  5. package/dist/examples/mcp-lafs-client.d.ts +10 -0
  6. package/dist/examples/mcp-lafs-client.js +427 -0
  7. package/dist/examples/mcp-lafs-server.d.ts +10 -0
  8. package/dist/examples/mcp-lafs-server.js +358 -0
  9. package/dist/schemas/v1/envelope.schema.json +0 -0
  10. package/dist/schemas/v1/error-registry.json +0 -0
  11. package/dist/src/a2a/bridge.d.ts +129 -0
  12. package/dist/src/a2a/bridge.js +173 -0
  13. package/dist/src/a2a/index.d.ts +36 -0
  14. package/dist/src/a2a/index.js +36 -0
  15. package/dist/src/budgetEnforcement.d.ts +84 -0
  16. package/dist/src/budgetEnforcement.js +328 -0
  17. package/dist/src/circuit-breaker/index.d.ts +121 -0
  18. package/dist/src/circuit-breaker/index.js +249 -0
  19. package/dist/src/cli.d.ts +0 -0
  20. package/dist/src/cli.js +0 -0
  21. package/dist/src/conformance.d.ts +0 -0
  22. package/dist/src/conformance.js +0 -0
  23. package/dist/src/discovery.d.ts +127 -0
  24. package/dist/src/discovery.js +304 -0
  25. package/dist/src/errorRegistry.d.ts +0 -0
  26. package/dist/src/errorRegistry.js +0 -0
  27. package/dist/src/flagSemantics.d.ts +0 -0
  28. package/dist/src/flagSemantics.js +0 -0
  29. package/dist/src/health/index.d.ts +105 -0
  30. package/dist/src/health/index.js +211 -0
  31. package/dist/src/index.d.ts +8 -0
  32. package/dist/src/index.js +10 -0
  33. package/dist/src/mcpAdapter.d.ts +28 -0
  34. package/dist/src/mcpAdapter.js +281 -0
  35. package/dist/src/shutdown/index.d.ts +69 -0
  36. package/dist/src/shutdown/index.js +160 -0
  37. package/dist/src/tokenEstimator.d.ts +87 -0
  38. package/dist/src/tokenEstimator.js +238 -0
  39. package/dist/src/types.d.ts +25 -0
  40. package/dist/src/types.js +0 -0
  41. package/dist/src/validateEnvelope.d.ts +0 -0
  42. package/dist/src/validateEnvelope.js +0 -0
  43. package/lafs.md +167 -0
  44. package/package.json +10 -4
  45. package/schemas/v1/context-ledger.schema.json +0 -0
  46. package/schemas/v1/discovery.schema.json +132 -0
  47. package/schemas/v1/envelope.schema.json +0 -0
  48. package/schemas/v1/error-registry.json +0 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * LAFS Graceful Shutdown Module
3
+ *
4
+ * Handles graceful shutdown of LAFS servers
5
+ */
6
+ import { Server } from 'http';
7
+ export interface GracefulShutdownConfig {
8
+ timeout?: number;
9
+ signals?: NodeJS.Signals[];
10
+ onShutdown?: () => Promise<void> | void;
11
+ onClose?: () => Promise<void> | void;
12
+ }
13
+ export interface ShutdownState {
14
+ isShuttingDown: boolean;
15
+ activeConnections: number;
16
+ shutdownStartTime?: Date;
17
+ }
18
+ /**
19
+ * Enable graceful shutdown for an HTTP server
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import express from 'express';
24
+ * import { gracefulShutdown } from '@cleocode/lafs-protocol/shutdown';
25
+ *
26
+ * const app = express();
27
+ * const server = app.listen(3000);
28
+ *
29
+ * gracefulShutdown(server, {
30
+ * timeout: 30000,
31
+ * signals: ['SIGTERM', 'SIGINT'],
32
+ * onShutdown: async () => {
33
+ * console.log('Shutting down...');
34
+ * await db.close();
35
+ * }
36
+ * });
37
+ * ```
38
+ */
39
+ export declare function gracefulShutdown(server: Server, config?: GracefulShutdownConfig): void;
40
+ /**
41
+ * Check if server is shutting down
42
+ */
43
+ export declare function isShuttingDown(): boolean;
44
+ /**
45
+ * Get shutdown state
46
+ */
47
+ export declare function getShutdownState(): ShutdownState;
48
+ /**
49
+ * Force immediate shutdown (emergency use only)
50
+ */
51
+ export declare function forceShutdown(exitCode?: number): void;
52
+ /**
53
+ * Middleware to reject requests during shutdown
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * app.use(shutdownMiddleware());
58
+ * ```
59
+ */
60
+ export declare function shutdownMiddleware(): (req: any, res: any, next: any) => void;
61
+ /**
62
+ * Wait for shutdown to complete
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * await waitForShutdown();
67
+ * ```
68
+ */
69
+ export declare function waitForShutdown(): Promise<void>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * LAFS Graceful Shutdown Module
3
+ *
4
+ * Handles graceful shutdown of LAFS servers
5
+ */
6
+ const state = {
7
+ isShuttingDown: false,
8
+ activeConnections: 0
9
+ };
10
+ /**
11
+ * Enable graceful shutdown for an HTTP server
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import express from 'express';
16
+ * import { gracefulShutdown } from '@cleocode/lafs-protocol/shutdown';
17
+ *
18
+ * const app = express();
19
+ * const server = app.listen(3000);
20
+ *
21
+ * gracefulShutdown(server, {
22
+ * timeout: 30000,
23
+ * signals: ['SIGTERM', 'SIGINT'],
24
+ * onShutdown: async () => {
25
+ * console.log('Shutting down...');
26
+ * await db.close();
27
+ * }
28
+ * });
29
+ * ```
30
+ */
31
+ export function gracefulShutdown(server, config = {}) {
32
+ const { timeout = 30000, signals = ['SIGTERM', 'SIGINT'], onShutdown, onClose } = config;
33
+ // Track active connections
34
+ server.on('connection', (socket) => {
35
+ if (state.isShuttingDown) {
36
+ socket.destroy();
37
+ return;
38
+ }
39
+ state.activeConnections++;
40
+ socket.on('close', () => {
41
+ state.activeConnections--;
42
+ });
43
+ });
44
+ // Handle shutdown signals
45
+ signals.forEach((signal) => {
46
+ process.on(signal, async () => {
47
+ console.log(`${signal} received, starting graceful shutdown...`);
48
+ await performShutdown(server, timeout, onShutdown, onClose);
49
+ });
50
+ });
51
+ // Handle uncaught errors
52
+ process.on('uncaughtException', async (error) => {
53
+ console.error('Uncaught exception:', error);
54
+ await performShutdown(server, timeout, onShutdown, onClose);
55
+ });
56
+ process.on('unhandledRejection', async (reason) => {
57
+ console.error('Unhandled rejection:', reason);
58
+ await performShutdown(server, timeout, onShutdown, onClose);
59
+ });
60
+ }
61
+ async function performShutdown(server, timeout, onShutdown, onClose) {
62
+ if (state.isShuttingDown) {
63
+ console.log('Shutdown already in progress...');
64
+ return;
65
+ }
66
+ state.isShuttingDown = true;
67
+ state.shutdownStartTime = new Date();
68
+ try {
69
+ // Call user shutdown handler
70
+ if (onShutdown) {
71
+ await Promise.race([
72
+ onShutdown(),
73
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), timeout))
74
+ ]);
75
+ }
76
+ // Stop accepting new connections
77
+ server.close((err) => {
78
+ if (err) {
79
+ console.error('Error closing server:', err);
80
+ }
81
+ else {
82
+ console.log('Server closed');
83
+ }
84
+ });
85
+ // Wait for active connections to close
86
+ const startTime = Date.now();
87
+ while (state.activeConnections > 0 && Date.now() - startTime < timeout) {
88
+ console.log(`Waiting for ${state.activeConnections} connections to close...`);
89
+ await sleep(1000);
90
+ }
91
+ if (state.activeConnections > 0) {
92
+ console.warn(`Forcing shutdown with ${state.activeConnections} active connections`);
93
+ }
94
+ // Call user close handler
95
+ if (onClose) {
96
+ await onClose();
97
+ }
98
+ console.log('Graceful shutdown complete');
99
+ process.exit(0);
100
+ }
101
+ catch (error) {
102
+ console.error('Error during shutdown:', error);
103
+ process.exit(1);
104
+ }
105
+ }
106
+ function sleep(ms) {
107
+ return new Promise(resolve => setTimeout(resolve, ms));
108
+ }
109
+ /**
110
+ * Check if server is shutting down
111
+ */
112
+ export function isShuttingDown() {
113
+ return state.isShuttingDown;
114
+ }
115
+ /**
116
+ * Get shutdown state
117
+ */
118
+ export function getShutdownState() {
119
+ return { ...state };
120
+ }
121
+ /**
122
+ * Force immediate shutdown (emergency use only)
123
+ */
124
+ export function forceShutdown(exitCode = 1) {
125
+ console.log('Force shutting down...');
126
+ process.exit(exitCode);
127
+ }
128
+ /**
129
+ * Middleware to reject requests during shutdown
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * app.use(shutdownMiddleware());
134
+ * ```
135
+ */
136
+ export function shutdownMiddleware() {
137
+ return (req, res, next) => {
138
+ if (state.isShuttingDown) {
139
+ res.status(503).json({
140
+ error: 'Service is shutting down',
141
+ status: 'unavailable'
142
+ });
143
+ return;
144
+ }
145
+ next();
146
+ };
147
+ }
148
+ /**
149
+ * Wait for shutdown to complete
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * await waitForShutdown();
154
+ * ```
155
+ */
156
+ export async function waitForShutdown() {
157
+ while (!state.isShuttingDown) {
158
+ await sleep(100);
159
+ }
160
+ }
@@ -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
+ }
@@ -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