@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,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
|
+
}
|
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
|