@cleocode/lafs-protocol 1.0.0 → 1.2.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 +7 -3
- package/dist/schemas/v1/envelope.schema.json +6 -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/circuit-breaker/index.d.ts +121 -0
- package/dist/src/circuit-breaker/index.js +249 -0
- package/dist/src/flagSemantics.d.ts +2 -0
- package/dist/src/flagSemantics.js +7 -6
- package/dist/src/health/index.d.ts +105 -0
- package/dist/src/health/index.js +211 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +6 -0
- package/dist/src/shutdown/index.d.ts +69 -0
- package/dist/src/shutdown/index.js +160 -0
- package/dist/src/types.d.ts +4 -0
- package/lafs.md +187 -0
- package/package.json +3 -2
- package/schemas/v1/envelope.schema.json +6 -0
|
@@ -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
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface LAFSMeta {
|
|
|
17
17
|
strict: boolean;
|
|
18
18
|
mvi: 'minimal' | 'standard' | 'full' | 'custom';
|
|
19
19
|
contextVersion: number;
|
|
20
|
+
/** Session identifier for correlating multi-step agent workflows */
|
|
21
|
+
sessionId?: string;
|
|
20
22
|
warnings?: Warning[];
|
|
21
23
|
}
|
|
22
24
|
export interface LAFSError {
|
|
@@ -76,6 +78,8 @@ export interface FlagInput {
|
|
|
76
78
|
humanFlag?: boolean;
|
|
77
79
|
projectDefault?: "json" | "human";
|
|
78
80
|
userDefault?: "json" | "human";
|
|
81
|
+
/** Suppress non-essential output for scripting. When true, only essential data is returned. */
|
|
82
|
+
quiet?: boolean;
|
|
79
83
|
}
|
|
80
84
|
export interface ConformanceReport {
|
|
81
85
|
ok: boolean;
|
package/lafs.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# LAFS: LLM-Agent-First Specification
|
|
2
2
|
|
|
3
|
+
> 📚 **Documentation:** https://codluv.gitbook.io/lafs-protocol/
|
|
4
|
+
> **Version:** 1.2.0 | **Status:** Production Ready
|
|
5
|
+
|
|
3
6
|
## 1. Scope
|
|
4
7
|
|
|
5
8
|
LAFS is a **response envelope contract specification**. It defines the canonical shape of structured responses — success envelopes, error envelopes, pagination metadata, and context preservation — for software systems whose primary consumer is an LLM agent or AI-driven tool.
|
|
@@ -64,6 +67,52 @@ The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are interpreted per RFC
|
|
|
64
67
|
3. Global/user config
|
|
65
68
|
4. Protocol default (`json`)
|
|
66
69
|
|
|
70
|
+
### 5.3 Supported formats
|
|
71
|
+
|
|
72
|
+
LAFS supports exactly two output formats:
|
|
73
|
+
|
|
74
|
+
- **`json`** (default) — Machine-readable JSON envelope for programmatic consumption
|
|
75
|
+
- **`human`** — Human-readable text output for terminal display
|
|
76
|
+
|
|
77
|
+
#### 5.3.1 Human format definition
|
|
78
|
+
|
|
79
|
+
The `human` format produces plain text output optimized for terminal display:
|
|
80
|
+
|
|
81
|
+
- Suitable for direct human consumption in CLI environments
|
|
82
|
+
- NOT markdown, NOT tables, NOT structured data
|
|
83
|
+
- May include ANSI colors (respect `NO_COLOR` environment variable)
|
|
84
|
+
- Example: Tabular data displayed with aligned columns using spaces
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
ID Name Status
|
|
88
|
+
---- ------------ --------
|
|
89
|
+
123 Alpha active
|
|
90
|
+
456 Beta pending
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 5.3.2 Rejected formats
|
|
94
|
+
|
|
95
|
+
The following formats were explicitly rejected to maintain protocol minimalism:
|
|
96
|
+
|
|
97
|
+
| Format | Status | Rationale |
|
|
98
|
+
|--------|--------|-----------|
|
|
99
|
+
| `text` | ❌ Rejected | Ambiguous overlap with `human`. Use `human` format with `NO_COLOR=1` for plain text. |
|
|
100
|
+
| `markdown` | ❌ Rejected | Presentation format, not data format. Generate from JSON if markdown rendering is needed. |
|
|
101
|
+
| `table` | ❌ Rejected | Presentation concern. Use `jq` + `column` command or `human` format for tabular display. |
|
|
102
|
+
| `jsonl` | ❌ Rejected | Streaming format violates LAFS discrete envelope contract (see Section 2: Non-Goals). |
|
|
103
|
+
|
|
104
|
+
**Design principle:** LAFS is a response envelope contract, not a presentation layer. Six formats = format proliferation = protocol bloat.
|
|
105
|
+
|
|
106
|
+
#### 5.3.3 Achieving presentation goals with json format
|
|
107
|
+
|
|
108
|
+
Consumers needing presentation formats should:
|
|
109
|
+
|
|
110
|
+
1. Request `json` format from LAFS-compliant services
|
|
111
|
+
2. Transform JSON to desired presentation format using standard tools:
|
|
112
|
+
- **Markdown:** `jq` + template engine
|
|
113
|
+
- **Tables:** `jq` + `column` command
|
|
114
|
+
- **Plain text:** `jq` with `-r` (raw) output
|
|
115
|
+
|
|
67
116
|
---
|
|
68
117
|
|
|
69
118
|
## 6. Canonical Response Envelope
|
|
@@ -107,6 +156,144 @@ The envelope supports an optional `_extensions` object for vendor-specific metad
|
|
|
107
156
|
- Consumers MUST NOT rely on extension fields for protocol-required behavior.
|
|
108
157
|
- Producers MAY omit `_extensions` entirely; the field is always optional.
|
|
109
158
|
|
|
159
|
+
#### 6.2.1 Extension use cases
|
|
160
|
+
|
|
161
|
+
The following examples demonstrate common use cases for `_extensions`. These fields were rejected from the core protocol but are valid extension use cases.
|
|
162
|
+
|
|
163
|
+
**Example 1: Performance timing**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Extension type definition
|
|
167
|
+
interface XTimingExtension {
|
|
168
|
+
"x-timing": {
|
|
169
|
+
executionMs: number; // Total request execution time
|
|
170
|
+
parseMs?: number; // Input parsing time
|
|
171
|
+
queryMs?: number; // Database query time
|
|
172
|
+
serializeMs?: number; // Response serialization time
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"_extensions": {
|
|
180
|
+
"x-timing": {
|
|
181
|
+
"executionMs": 42,
|
|
182
|
+
"queryMs": 15,
|
|
183
|
+
"serializeMs": 3
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Example 2: Source metadata**
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface XSourceExtension {
|
|
193
|
+
"x-source": {
|
|
194
|
+
gitRef?: string; // Git commit SHA
|
|
195
|
+
apiVersion?: string; // API implementation version
|
|
196
|
+
buildTimestamp?: string; // ISO 8601 build time
|
|
197
|
+
deployment?: string; // Deployment environment (staging, prod)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"_extensions": {
|
|
205
|
+
"x-source": {
|
|
206
|
+
"gitRef": "abc123def456",
|
|
207
|
+
"apiVersion": "2.1.0",
|
|
208
|
+
"deployment": "production"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Example 3: Applied filters**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
interface XFiltersExtension {
|
|
218
|
+
"x-filters": {
|
|
219
|
+
applied: Array<{
|
|
220
|
+
field: string;
|
|
221
|
+
operator: "eq" | "neq" | "gt" | "lt" | "contains";
|
|
222
|
+
value: unknown;
|
|
223
|
+
}>;
|
|
224
|
+
omitted: string[]; // Fields excluded due to permissions
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"_extensions": {
|
|
232
|
+
"x-filters": {
|
|
233
|
+
"applied": [
|
|
234
|
+
{ "field": "status", "operator": "eq", "value": "active" },
|
|
235
|
+
{ "field": "createdAt", "operator": "gt", "value": "2024-01-01" }
|
|
236
|
+
],
|
|
237
|
+
"omitted": ["internalNotes", "costCenter"]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Example 4: Result summary**
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
interface XSummaryExtension {
|
|
247
|
+
"x-summary": {
|
|
248
|
+
totalCount: number; // Total matching records
|
|
249
|
+
returnedCount: number; // Records in this response
|
|
250
|
+
aggregated?: {
|
|
251
|
+
revenue?: number;
|
|
252
|
+
count?: number;
|
|
253
|
+
average?: number;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"_extensions": {
|
|
262
|
+
"x-summary": {
|
|
263
|
+
"totalCount": 150,
|
|
264
|
+
"returnedCount": 25,
|
|
265
|
+
"aggregated": {
|
|
266
|
+
"revenue": 125000.00,
|
|
267
|
+
"average": 833.33
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### 6.2.2 Extension best practices
|
|
275
|
+
|
|
276
|
+
1. **Use x- prefix** — All extension keys MUST start with `x-` (e.g., `x-caamp-timing`)
|
|
277
|
+
2. **Document your schema** — Publish extension schemas separately from LAFS core
|
|
278
|
+
3. **Don't rely on extensions for core behavior** — Extensions are informational only
|
|
279
|
+
4. **Version your extensions** — Include version in extension key if schema may change (e.g., `x-vendor-v2-field`)
|
|
280
|
+
5. **Keep extensions optional** — Consumers MUST be able to operate without extension data
|
|
281
|
+
6. **Namespace by vendor** — Use vendor prefix to avoid collisions (e.g., `x-caamp-`, `x-acme-`)
|
|
282
|
+
|
|
283
|
+
#### 6.2.3 When to use extensions vs core protocol
|
|
284
|
+
|
|
285
|
+
| Use Case | Core Protocol | Extensions |
|
|
286
|
+
|----------|---------------|------------|
|
|
287
|
+
| Session correlation | ✅ sessionId | — |
|
|
288
|
+
| Soft warnings | ✅ warnings array | — |
|
|
289
|
+
| Performance timing | — | ✅ x-timing |
|
|
290
|
+
| Source/version metadata | — | ✅ x-source |
|
|
291
|
+
| Debug filters | — | ✅ x-filters |
|
|
292
|
+
| Derived summaries | — | ✅ x-summary |
|
|
293
|
+
| Data integrity (TLS covers) | — | ✅ x-checksum (if needed) |
|
|
294
|
+
|
|
295
|
+
**Guideline:** If a field is required for basic operation, it belongs in core. If it's useful for debugging, monitoring, or rich display, it belongs in extensions.
|
|
296
|
+
|
|
110
297
|
---
|
|
111
298
|
|
|
112
299
|
## 7. Error Contract
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/lafs-protocol",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "LLM-Agent-First Specification schemas and conformance tooling",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"type": "git",
|
|
24
24
|
"url": "git+https://github.com/kryptobaseddev/lafs-protocol.git"
|
|
25
25
|
},
|
|
26
|
-
"homepage": "https://
|
|
26
|
+
"homepage": "https://codluv.gitbook.io/lafs-protocol/",
|
|
27
27
|
"bugs": {
|
|
28
28
|
"url": "https://github.com/kryptobaseddev/lafs-protocol/issues"
|
|
29
29
|
},
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"vitest": "^2.1.9"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"@a2a-js/sdk": "^0.3.10",
|
|
63
64
|
"ajv": "^8.18.0",
|
|
64
65
|
"ajv-formats": "^3.0.1",
|
|
65
66
|
"express": "^5.2.1"
|
|
@@ -67,6 +67,12 @@
|
|
|
67
67
|
"type": "integer",
|
|
68
68
|
"minimum": 0
|
|
69
69
|
},
|
|
70
|
+
"sessionId": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"minLength": 1,
|
|
73
|
+
"maxLength": 256,
|
|
74
|
+
"description": "Session identifier for correlating multi-step agent workflows"
|
|
75
|
+
},
|
|
70
76
|
"warnings": {
|
|
71
77
|
"type": "array",
|
|
72
78
|
"items": {
|