@cleocode/lafs-protocol 1.0.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/README.md +7 -3
- 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/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/lafs.md +3 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
LAFS defines a standard envelope format for structured responses from LLM-powered agents and tools. It complements transport protocols like [MCP](https://modelcontextprotocol.io/) and [A2A](https://github.com/google/A2A) by standardizing what comes back — not how it gets there.
|
|
6
6
|
|
|
7
|
-
**Current version:** 0.
|
|
7
|
+
**Current version:** 1.0.0 | [📚 Documentation](https://codluv.gitbook.io/lafs-protocol/) | [Spec](lafs.md) | [Migration Guides](migrations/)
|
|
8
|
+
|
|
9
|
+
[](https://codluv.gitbook.io/lafs-protocol/)
|
|
10
|
+
[](https://www.npmjs.com/package/@cleocode/lafs-protocol)
|
|
8
11
|
|
|
9
12
|
## What LAFS provides
|
|
10
13
|
|
|
@@ -17,7 +20,7 @@ LAFS defines a standard envelope format for structured responses from LLM-powere
|
|
|
17
20
|
| **Tooling** | `src/` | TypeScript validation, conformance runner, CLI diagnostic tool |
|
|
18
21
|
| **Tests** | `tests/` | 31 tests covering envelope, pagination, strict mode, error handling |
|
|
19
22
|
| **Fixtures** | `fixtures/` | 14 JSON fixtures (valid + invalid) for conformance testing |
|
|
20
|
-
| **Docs** | `docs/` |
|
|
23
|
+
| **Docs** | `docs/` | [GitBook documentation](https://codluv.gitbook.io/lafs-protocol/) with guides, SDK reference, and specs |
|
|
21
24
|
|
|
22
25
|
## Install
|
|
23
26
|
|
|
@@ -64,7 +67,7 @@ npm run typecheck
|
|
|
64
67
|
{
|
|
65
68
|
"$schema": "https://lafs.dev/schemas/v1/envelope.schema.json",
|
|
66
69
|
"_meta": {
|
|
67
|
-
"specVersion": "0.
|
|
70
|
+
"specVersion": "1.0.0",
|
|
68
71
|
"schemaVersion": "1.0.0",
|
|
69
72
|
"timestamp": "2026-02-13T00:00:00Z",
|
|
70
73
|
"operation": "example.list",
|
|
@@ -139,6 +142,7 @@ CONTRIBUTING.md # Contributor guidelines, RFC process
|
|
|
139
142
|
|
|
140
143
|
| Version | Phase | Description |
|
|
141
144
|
|---------|-------|-------------|
|
|
145
|
+
| **v1.0.0** | **3** | **Production release: Token budgets, agent discovery, MCP integration, complete SDKs** |
|
|
142
146
|
| v0.5.0 | 2B | Conditional pagination, MVI field selection/expansion, context ledger schema |
|
|
143
147
|
| v0.4.0 | 2A | Optional page/error, extensions, strict/lenient mode, warnings |
|
|
144
148
|
| v0.3.0 | 1 | Strategic positioning, vision alignment, adoption tiers |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS A2A Bridge
|
|
3
|
+
*
|
|
4
|
+
* Integration with official @a2a-js/sdk for Agent-to-Agent communication.
|
|
5
|
+
* LAFS provides envelope wrapping and token budget support.
|
|
6
|
+
*/
|
|
7
|
+
import { A2AClient } from '@a2a-js/sdk/client';
|
|
8
|
+
import { Artifact, Part, SendMessageResponse, JSONRPCErrorResponse } from '@a2a-js/sdk';
|
|
9
|
+
export interface LafsA2AConfig {
|
|
10
|
+
defaultBudget?: {
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
maxItems?: number;
|
|
13
|
+
};
|
|
14
|
+
envelopeResponses?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface LafsEnvelope {
|
|
17
|
+
$schema: string;
|
|
18
|
+
_meta: {
|
|
19
|
+
specVersion: string;
|
|
20
|
+
operation: string;
|
|
21
|
+
requestId: string;
|
|
22
|
+
mvi: string;
|
|
23
|
+
_tokenEstimate?: {
|
|
24
|
+
estimated: number;
|
|
25
|
+
budget?: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
success: boolean;
|
|
29
|
+
result: unknown;
|
|
30
|
+
error: null | {
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
category: string;
|
|
34
|
+
retryable: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wrap A2A client with LAFS envelope support
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
43
|
+
* import { withLafsEnvelope } from '@lafs/envelope/a2a';
|
|
44
|
+
*
|
|
45
|
+
* const factory = new ClientFactory();
|
|
46
|
+
* const a2aClient = await factory.createFromUrl('http://localhost:4000');
|
|
47
|
+
*
|
|
48
|
+
* const client = withLafsEnvelope(a2aClient, {
|
|
49
|
+
* envelopeResponses: true,
|
|
50
|
+
* defaultBudget: { maxTokens: 4000 }
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* const result = await client.sendMessage({
|
|
54
|
+
* message: { role: 'user', parts: [{ text: 'Hello' }] }
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Access LAFS envelope
|
|
58
|
+
* const envelope = result.getLafsEnvelope();
|
|
59
|
+
* console.log(envelope._meta._tokenEstimate);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function withLafsEnvelope(client: A2AClient, config?: LafsA2AConfig): LafsA2AClient;
|
|
63
|
+
export declare class LafsA2AClient {
|
|
64
|
+
private client;
|
|
65
|
+
private config;
|
|
66
|
+
constructor(client: A2AClient, config: LafsA2AConfig);
|
|
67
|
+
sendMessage(params: {
|
|
68
|
+
message: {
|
|
69
|
+
role: 'user' | 'agent';
|
|
70
|
+
parts: Part[];
|
|
71
|
+
};
|
|
72
|
+
budget?: {
|
|
73
|
+
maxTokens?: number;
|
|
74
|
+
maxItems?: number;
|
|
75
|
+
};
|
|
76
|
+
}): Promise<LafsA2AResult>;
|
|
77
|
+
private generateId;
|
|
78
|
+
}
|
|
79
|
+
export declare class LafsA2AResult {
|
|
80
|
+
private result;
|
|
81
|
+
private budget;
|
|
82
|
+
constructor(result: SendMessageResponse, budget: {
|
|
83
|
+
maxTokens?: number;
|
|
84
|
+
maxItems?: number;
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* Get the underlying A2A result
|
|
88
|
+
*/
|
|
89
|
+
getA2AResult(): SendMessageResponse;
|
|
90
|
+
/**
|
|
91
|
+
* Check if result is an error
|
|
92
|
+
*/
|
|
93
|
+
isError(): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Get error details if result is an error
|
|
96
|
+
*/
|
|
97
|
+
getError(): JSONRPCErrorResponse | null;
|
|
98
|
+
/**
|
|
99
|
+
* Extract LAFS envelope from A2A artifact
|
|
100
|
+
*/
|
|
101
|
+
getLafsEnvelope(): LafsEnvelope | null;
|
|
102
|
+
/**
|
|
103
|
+
* Check if result contains LAFS envelope
|
|
104
|
+
*/
|
|
105
|
+
hasLafsEnvelope(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Get token estimate from envelope
|
|
108
|
+
*/
|
|
109
|
+
getTokenEstimate(): {
|
|
110
|
+
estimated: number;
|
|
111
|
+
budget?: number;
|
|
112
|
+
} | null;
|
|
113
|
+
private isLafsEnvelope;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a LAFS artifact for A2A
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const artifact = createLafsArtifact({
|
|
121
|
+
* success: true,
|
|
122
|
+
* result: { data: '...' },
|
|
123
|
+
* meta: { operation: 'analysis.run' }
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* task.artifacts.push(artifact);
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function createLafsArtifact(envelope: LafsEnvelope): Artifact;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS A2A Bridge
|
|
3
|
+
*
|
|
4
|
+
* Integration with official @a2a-js/sdk for Agent-to-Agent communication.
|
|
5
|
+
* LAFS provides envelope wrapping and token budget support.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Wrap A2A client with LAFS envelope support
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
13
|
+
* import { withLafsEnvelope } from '@lafs/envelope/a2a';
|
|
14
|
+
*
|
|
15
|
+
* const factory = new ClientFactory();
|
|
16
|
+
* const a2aClient = await factory.createFromUrl('http://localhost:4000');
|
|
17
|
+
*
|
|
18
|
+
* const client = withLafsEnvelope(a2aClient, {
|
|
19
|
+
* envelopeResponses: true,
|
|
20
|
+
* defaultBudget: { maxTokens: 4000 }
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const result = await client.sendMessage({
|
|
24
|
+
* message: { role: 'user', parts: [{ text: 'Hello' }] }
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Access LAFS envelope
|
|
28
|
+
* const envelope = result.getLafsEnvelope();
|
|
29
|
+
* console.log(envelope._meta._tokenEstimate);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function withLafsEnvelope(client, config = {}) {
|
|
33
|
+
return new LafsA2AClient(client, config);
|
|
34
|
+
}
|
|
35
|
+
export class LafsA2AClient {
|
|
36
|
+
client;
|
|
37
|
+
config;
|
|
38
|
+
constructor(client, config) {
|
|
39
|
+
this.client = client;
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
async sendMessage(params) {
|
|
43
|
+
// Merge budget with defaults
|
|
44
|
+
const budget = {
|
|
45
|
+
...this.config.defaultBudget,
|
|
46
|
+
...params.budget
|
|
47
|
+
};
|
|
48
|
+
// Send via official A2A SDK
|
|
49
|
+
const result = await this.client.sendMessage({
|
|
50
|
+
message: {
|
|
51
|
+
kind: 'message',
|
|
52
|
+
messageId: this.generateId(),
|
|
53
|
+
role: params.message.role,
|
|
54
|
+
parts: params.message.parts
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// Wrap result with LAFS envelope support
|
|
58
|
+
return new LafsA2AResult(result, budget);
|
|
59
|
+
}
|
|
60
|
+
generateId() {
|
|
61
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
62
|
+
const r = Math.random() * 16 | 0;
|
|
63
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
64
|
+
return v.toString(16);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class LafsA2AResult {
|
|
69
|
+
result;
|
|
70
|
+
budget;
|
|
71
|
+
constructor(result, budget) {
|
|
72
|
+
this.result = result;
|
|
73
|
+
this.budget = budget;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get the underlying A2A result
|
|
77
|
+
*/
|
|
78
|
+
getA2AResult() {
|
|
79
|
+
return this.result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if result is an error
|
|
83
|
+
*/
|
|
84
|
+
isError() {
|
|
85
|
+
return 'error' in this.result;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get error details if result is an error
|
|
89
|
+
*/
|
|
90
|
+
getError() {
|
|
91
|
+
if (this.isError()) {
|
|
92
|
+
return this.result;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extract LAFS envelope from A2A artifact
|
|
98
|
+
*/
|
|
99
|
+
getLafsEnvelope() {
|
|
100
|
+
if (this.isError()) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const successResult = this.result;
|
|
104
|
+
// Check if result is a Task
|
|
105
|
+
if (successResult.result?.kind !== 'task') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const task = successResult.result;
|
|
109
|
+
if (!task.artifacts || task.artifacts.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Find LAFS envelope in artifacts
|
|
113
|
+
for (const artifact of task.artifacts) {
|
|
114
|
+
for (const part of artifact.parts) {
|
|
115
|
+
if (part.kind === 'data' && this.isLafsEnvelope(part.data)) {
|
|
116
|
+
return part.data;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if result contains LAFS envelope
|
|
124
|
+
*/
|
|
125
|
+
hasLafsEnvelope() {
|
|
126
|
+
return this.getLafsEnvelope() !== null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get token estimate from envelope
|
|
130
|
+
*/
|
|
131
|
+
getTokenEstimate() {
|
|
132
|
+
const envelope = this.getLafsEnvelope();
|
|
133
|
+
return envelope?._meta?._tokenEstimate ?? null;
|
|
134
|
+
}
|
|
135
|
+
isLafsEnvelope(data) {
|
|
136
|
+
return (typeof data === 'object' &&
|
|
137
|
+
data !== null &&
|
|
138
|
+
'$schema' in data &&
|
|
139
|
+
'_meta' in data &&
|
|
140
|
+
'success' in data);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Create a LAFS artifact for A2A
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const artifact = createLafsArtifact({
|
|
149
|
+
* success: true,
|
|
150
|
+
* result: { data: '...' },
|
|
151
|
+
* meta: { operation: 'analysis.run' }
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* task.artifacts.push(artifact);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createLafsArtifact(envelope) {
|
|
158
|
+
return {
|
|
159
|
+
artifactId: generateId(),
|
|
160
|
+
name: 'lafs_response',
|
|
161
|
+
parts: [{
|
|
162
|
+
kind: 'data',
|
|
163
|
+
data: envelope
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function generateId() {
|
|
168
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
169
|
+
const r = Math.random() * 16 | 0;
|
|
170
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
171
|
+
return v.toString(16);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Agent-to-Agent (A2A) Integration
|
|
3
|
+
*
|
|
4
|
+
* This module provides integration between LAFS and the official
|
|
5
|
+
* @a2a-js/sdk for Agent-to-Agent communication.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
10
|
+
* import { withLafsEnvelope } from '@cleocode/lafs-protocol/a2a';
|
|
11
|
+
*
|
|
12
|
+
* // Create official A2A client
|
|
13
|
+
* const factory = new ClientFactory();
|
|
14
|
+
* const a2aClient = await factory.createFromUrl('http://agent.example.com');
|
|
15
|
+
*
|
|
16
|
+
* // Wrap with LAFS support
|
|
17
|
+
* const client = withLafsEnvelope(a2aClient, {
|
|
18
|
+
* defaultBudget: { maxTokens: 4000 }
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Send message
|
|
22
|
+
* const result = await client.sendMessage({
|
|
23
|
+
* message: {
|
|
24
|
+
* role: 'user',
|
|
25
|
+
* parts: [{ text: 'Analyze data' }]
|
|
26
|
+
* }
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Extract LAFS envelope from response
|
|
30
|
+
* const envelope = result.getLafsEnvelope();
|
|
31
|
+
* if (envelope) {
|
|
32
|
+
* console.log(envelope._meta._tokenEstimate);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export { withLafsEnvelope, LafsA2AClient, LafsA2AResult, createLafsArtifact, type LafsA2AConfig, type LafsEnvelope } from './bridge.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Agent-to-Agent (A2A) Integration
|
|
3
|
+
*
|
|
4
|
+
* This module provides integration between LAFS and the official
|
|
5
|
+
* @a2a-js/sdk for Agent-to-Agent communication.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ClientFactory } from '@a2a-js/sdk/client';
|
|
10
|
+
* import { withLafsEnvelope } from '@cleocode/lafs-protocol/a2a';
|
|
11
|
+
*
|
|
12
|
+
* // Create official A2A client
|
|
13
|
+
* const factory = new ClientFactory();
|
|
14
|
+
* const a2aClient = await factory.createFromUrl('http://agent.example.com');
|
|
15
|
+
*
|
|
16
|
+
* // Wrap with LAFS support
|
|
17
|
+
* const client = withLafsEnvelope(a2aClient, {
|
|
18
|
+
* defaultBudget: { maxTokens: 4000 }
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Send message
|
|
22
|
+
* const result = await client.sendMessage({
|
|
23
|
+
* message: {
|
|
24
|
+
* role: 'user',
|
|
25
|
+
* parts: [{ text: 'Analyze data' }]
|
|
26
|
+
* }
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Extract LAFS envelope from response
|
|
30
|
+
* const envelope = result.getLafsEnvelope();
|
|
31
|
+
* if (envelope) {
|
|
32
|
+
* console.log(envelope._meta._tokenEstimate);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export { withLafsEnvelope, LafsA2AClient, LafsA2AResult, createLafsArtifact } from './bridge.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Circuit Breaker Module
|
|
3
|
+
*
|
|
4
|
+
* Provides circuit breaker pattern for resilient service calls
|
|
5
|
+
*/
|
|
6
|
+
export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
7
|
+
export interface CircuitBreakerConfig {
|
|
8
|
+
name: string;
|
|
9
|
+
failureThreshold?: number;
|
|
10
|
+
resetTimeout?: number;
|
|
11
|
+
halfOpenMaxCalls?: number;
|
|
12
|
+
successThreshold?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CircuitBreakerMetrics {
|
|
15
|
+
state: CircuitState;
|
|
16
|
+
failures: number;
|
|
17
|
+
successes: number;
|
|
18
|
+
lastFailureTime?: Date;
|
|
19
|
+
consecutiveSuccesses: number;
|
|
20
|
+
totalCalls: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class CircuitBreakerError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Circuit breaker for protecting against cascading failures
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { CircuitBreaker } from '@cleocode/lafs-protocol/circuit-breaker';
|
|
31
|
+
*
|
|
32
|
+
* const breaker = new CircuitBreaker({
|
|
33
|
+
* name: 'external-api',
|
|
34
|
+
* failureThreshold: 5,
|
|
35
|
+
* resetTimeout: 30000
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* try {
|
|
39
|
+
* const result = await breaker.execute(async () => {
|
|
40
|
+
* return await externalApi.call();
|
|
41
|
+
* });
|
|
42
|
+
* } catch (error) {
|
|
43
|
+
* if (error instanceof CircuitBreakerError) {
|
|
44
|
+
* console.log('Circuit breaker is open');
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare class CircuitBreaker {
|
|
50
|
+
private config;
|
|
51
|
+
private state;
|
|
52
|
+
private failures;
|
|
53
|
+
private successes;
|
|
54
|
+
private lastFailureTime?;
|
|
55
|
+
private consecutiveSuccesses;
|
|
56
|
+
private totalCalls;
|
|
57
|
+
private halfOpenCalls;
|
|
58
|
+
private resetTimer?;
|
|
59
|
+
constructor(config: CircuitBreakerConfig);
|
|
60
|
+
/**
|
|
61
|
+
* Execute a function with circuit breaker protection
|
|
62
|
+
*/
|
|
63
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
64
|
+
/**
|
|
65
|
+
* Get current circuit breaker state
|
|
66
|
+
*/
|
|
67
|
+
getState(): CircuitState;
|
|
68
|
+
/**
|
|
69
|
+
* Get circuit breaker metrics
|
|
70
|
+
*/
|
|
71
|
+
getMetrics(): CircuitBreakerMetrics;
|
|
72
|
+
/**
|
|
73
|
+
* Manually open the circuit breaker
|
|
74
|
+
*/
|
|
75
|
+
forceOpen(): void;
|
|
76
|
+
/**
|
|
77
|
+
* Manually close the circuit breaker
|
|
78
|
+
*/
|
|
79
|
+
forceClose(): void;
|
|
80
|
+
private onSuccess;
|
|
81
|
+
private onFailure;
|
|
82
|
+
private transitionTo;
|
|
83
|
+
private shouldAttemptReset;
|
|
84
|
+
private scheduleReset;
|
|
85
|
+
private reset;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Circuit breaker registry for managing multiple breakers
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const registry = new CircuitBreakerRegistry();
|
|
93
|
+
*
|
|
94
|
+
* registry.add('payment-api', {
|
|
95
|
+
* failureThreshold: 3,
|
|
96
|
+
* resetTimeout: 60000
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* const paymentBreaker = registry.get('payment-api');
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare class CircuitBreakerRegistry {
|
|
103
|
+
private breakers;
|
|
104
|
+
add(name: string, config: Omit<CircuitBreakerConfig, 'name'>): CircuitBreaker;
|
|
105
|
+
get(name: string): CircuitBreaker | undefined;
|
|
106
|
+
getOrCreate(name: string, config: Omit<CircuitBreakerConfig, 'name'>): CircuitBreaker;
|
|
107
|
+
getAllMetrics(): Record<string, CircuitBreakerMetrics>;
|
|
108
|
+
resetAll(): void;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create a circuit breaker middleware for Express
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* app.use('/external-api', circuitBreakerMiddleware({
|
|
116
|
+
* name: 'external-api',
|
|
117
|
+
* failureThreshold: 5
|
|
118
|
+
* }));
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function circuitBreakerMiddleware(config: CircuitBreakerConfig): (req: any, res: any, next: any) => Promise<void>;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Circuit Breaker Module
|
|
3
|
+
*
|
|
4
|
+
* Provides circuit breaker pattern for resilient service calls
|
|
5
|
+
*/
|
|
6
|
+
export class CircuitBreakerError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'CircuitBreakerError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Circuit breaker for protecting against cascading failures
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { CircuitBreaker } from '@cleocode/lafs-protocol/circuit-breaker';
|
|
18
|
+
*
|
|
19
|
+
* const breaker = new CircuitBreaker({
|
|
20
|
+
* name: 'external-api',
|
|
21
|
+
* failureThreshold: 5,
|
|
22
|
+
* resetTimeout: 30000
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* try {
|
|
26
|
+
* const result = await breaker.execute(async () => {
|
|
27
|
+
* return await externalApi.call();
|
|
28
|
+
* });
|
|
29
|
+
* } catch (error) {
|
|
30
|
+
* if (error instanceof CircuitBreakerError) {
|
|
31
|
+
* console.log('Circuit breaker is open');
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class CircuitBreaker {
|
|
37
|
+
config;
|
|
38
|
+
state = 'CLOSED';
|
|
39
|
+
failures = 0;
|
|
40
|
+
successes = 0;
|
|
41
|
+
lastFailureTime;
|
|
42
|
+
consecutiveSuccesses = 0;
|
|
43
|
+
totalCalls = 0;
|
|
44
|
+
halfOpenCalls = 0;
|
|
45
|
+
resetTimer;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.config = {
|
|
49
|
+
failureThreshold: 5,
|
|
50
|
+
resetTimeout: 30000,
|
|
51
|
+
halfOpenMaxCalls: 3,
|
|
52
|
+
successThreshold: 2,
|
|
53
|
+
...config
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Execute a function with circuit breaker protection
|
|
58
|
+
*/
|
|
59
|
+
async execute(fn) {
|
|
60
|
+
this.totalCalls++;
|
|
61
|
+
if (this.state === 'OPEN') {
|
|
62
|
+
if (this.shouldAttemptReset()) {
|
|
63
|
+
this.transitionTo('HALF_OPEN');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is OPEN`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (this.state === 'HALF_OPEN') {
|
|
70
|
+
if (this.halfOpenCalls >= (this.config.halfOpenMaxCalls || 3)) {
|
|
71
|
+
throw new CircuitBreakerError(`Circuit breaker '${this.config.name}' is HALF_OPEN (max calls reached)`);
|
|
72
|
+
}
|
|
73
|
+
this.halfOpenCalls++;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const result = await fn();
|
|
77
|
+
this.onSuccess();
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.onFailure();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get current circuit breaker state
|
|
87
|
+
*/
|
|
88
|
+
getState() {
|
|
89
|
+
return this.state;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get circuit breaker metrics
|
|
93
|
+
*/
|
|
94
|
+
getMetrics() {
|
|
95
|
+
return {
|
|
96
|
+
state: this.state,
|
|
97
|
+
failures: this.failures,
|
|
98
|
+
successes: this.successes,
|
|
99
|
+
lastFailureTime: this.lastFailureTime,
|
|
100
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
101
|
+
totalCalls: this.totalCalls
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Manually open the circuit breaker
|
|
106
|
+
*/
|
|
107
|
+
forceOpen() {
|
|
108
|
+
this.transitionTo('OPEN');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Manually close the circuit breaker
|
|
112
|
+
*/
|
|
113
|
+
forceClose() {
|
|
114
|
+
this.transitionTo('CLOSED');
|
|
115
|
+
this.reset();
|
|
116
|
+
}
|
|
117
|
+
onSuccess() {
|
|
118
|
+
this.successes++;
|
|
119
|
+
this.consecutiveSuccesses++;
|
|
120
|
+
if (this.state === 'HALF_OPEN') {
|
|
121
|
+
if (this.consecutiveSuccesses >= (this.config.successThreshold || 2)) {
|
|
122
|
+
this.transitionTo('CLOSED');
|
|
123
|
+
this.reset();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
onFailure() {
|
|
128
|
+
this.failures++;
|
|
129
|
+
this.consecutiveSuccesses = 0;
|
|
130
|
+
this.lastFailureTime = new Date();
|
|
131
|
+
if (this.state === 'HALF_OPEN') {
|
|
132
|
+
this.transitionTo('OPEN');
|
|
133
|
+
this.scheduleReset();
|
|
134
|
+
}
|
|
135
|
+
else if (this.state === 'CLOSED') {
|
|
136
|
+
if (this.failures >= (this.config.failureThreshold || 5)) {
|
|
137
|
+
this.transitionTo('OPEN');
|
|
138
|
+
this.scheduleReset();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
transitionTo(newState) {
|
|
143
|
+
console.log(`Circuit breaker '${this.config.name}': ${this.state} -> ${newState}`);
|
|
144
|
+
this.state = newState;
|
|
145
|
+
if (newState === 'HALF_OPEN') {
|
|
146
|
+
this.halfOpenCalls = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
shouldAttemptReset() {
|
|
150
|
+
if (!this.lastFailureTime)
|
|
151
|
+
return true;
|
|
152
|
+
const elapsed = Date.now() - this.lastFailureTime.getTime();
|
|
153
|
+
return elapsed >= (this.config.resetTimeout || 30000);
|
|
154
|
+
}
|
|
155
|
+
scheduleReset() {
|
|
156
|
+
if (this.resetTimer) {
|
|
157
|
+
clearTimeout(this.resetTimer);
|
|
158
|
+
}
|
|
159
|
+
this.resetTimer = setTimeout(() => {
|
|
160
|
+
if (this.state === 'OPEN') {
|
|
161
|
+
this.transitionTo('HALF_OPEN');
|
|
162
|
+
}
|
|
163
|
+
}, this.config.resetTimeout || 30000);
|
|
164
|
+
}
|
|
165
|
+
reset() {
|
|
166
|
+
this.failures = 0;
|
|
167
|
+
this.consecutiveSuccesses = 0;
|
|
168
|
+
this.halfOpenCalls = 0;
|
|
169
|
+
if (this.resetTimer) {
|
|
170
|
+
clearTimeout(this.resetTimer);
|
|
171
|
+
this.resetTimer = undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Circuit breaker registry for managing multiple breakers
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* const registry = new CircuitBreakerRegistry();
|
|
181
|
+
*
|
|
182
|
+
* registry.add('payment-api', {
|
|
183
|
+
* failureThreshold: 3,
|
|
184
|
+
* resetTimeout: 60000
|
|
185
|
+
* });
|
|
186
|
+
*
|
|
187
|
+
* const paymentBreaker = registry.get('payment-api');
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export class CircuitBreakerRegistry {
|
|
191
|
+
breakers = new Map();
|
|
192
|
+
add(name, config) {
|
|
193
|
+
const breaker = new CircuitBreaker({ ...config, name });
|
|
194
|
+
this.breakers.set(name, breaker);
|
|
195
|
+
return breaker;
|
|
196
|
+
}
|
|
197
|
+
get(name) {
|
|
198
|
+
return this.breakers.get(name);
|
|
199
|
+
}
|
|
200
|
+
getOrCreate(name, config) {
|
|
201
|
+
let breaker = this.breakers.get(name);
|
|
202
|
+
if (!breaker) {
|
|
203
|
+
breaker = this.add(name, config);
|
|
204
|
+
}
|
|
205
|
+
return breaker;
|
|
206
|
+
}
|
|
207
|
+
getAllMetrics() {
|
|
208
|
+
const metrics = {};
|
|
209
|
+
this.breakers.forEach((breaker, name) => {
|
|
210
|
+
metrics[name] = breaker.getMetrics();
|
|
211
|
+
});
|
|
212
|
+
return metrics;
|
|
213
|
+
}
|
|
214
|
+
resetAll() {
|
|
215
|
+
this.breakers.forEach(breaker => breaker.forceClose());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Create a circuit breaker middleware for Express
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* app.use('/external-api', circuitBreakerMiddleware({
|
|
224
|
+
* name: 'external-api',
|
|
225
|
+
* failureThreshold: 5
|
|
226
|
+
* }));
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function circuitBreakerMiddleware(config) {
|
|
230
|
+
const breaker = new CircuitBreaker(config);
|
|
231
|
+
return async (req, res, next) => {
|
|
232
|
+
try {
|
|
233
|
+
await breaker.execute(async () => {
|
|
234
|
+
next();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error instanceof CircuitBreakerError) {
|
|
239
|
+
res.status(503).json({
|
|
240
|
+
error: 'Service temporarily unavailable',
|
|
241
|
+
reason: 'Circuit breaker is open'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
export interface HealthCheckConfig {
|
|
7
|
+
path?: string;
|
|
8
|
+
checks?: HealthCheckFunction[];
|
|
9
|
+
}
|
|
10
|
+
export type HealthCheckFunction = () => Promise<HealthCheckResult> | HealthCheckResult;
|
|
11
|
+
export interface HealthCheckResult {
|
|
12
|
+
name: string;
|
|
13
|
+
status: 'ok' | 'warning' | 'error';
|
|
14
|
+
message?: string;
|
|
15
|
+
duration?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HealthStatus {
|
|
18
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
19
|
+
timestamp: string;
|
|
20
|
+
version: string;
|
|
21
|
+
uptime: number;
|
|
22
|
+
checks: HealthCheckResult[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Health check middleware for Express applications
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import express from 'express';
|
|
30
|
+
* import { healthCheck } from '@cleocode/lafs-protocol/health';
|
|
31
|
+
*
|
|
32
|
+
* const app = express();
|
|
33
|
+
*
|
|
34
|
+
* // Basic health check
|
|
35
|
+
* app.use('/health', healthCheck());
|
|
36
|
+
*
|
|
37
|
+
* // Custom health checks
|
|
38
|
+
* app.use('/health', healthCheck({
|
|
39
|
+
* checks: [
|
|
40
|
+
* async () => ({
|
|
41
|
+
* name: 'database',
|
|
42
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
43
|
+
* })
|
|
44
|
+
* ]
|
|
45
|
+
* }));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function healthCheck(config?: HealthCheckConfig): (req: any, res: any) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Create a database health check
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
55
|
+
* checkConnection: async () => await db.ping()
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* app.use('/health', healthCheck({
|
|
59
|
+
* checks: [dbCheck]
|
|
60
|
+
* }));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDatabaseHealthCheck(config: {
|
|
64
|
+
checkConnection: () => Promise<boolean>;
|
|
65
|
+
name?: string;
|
|
66
|
+
}): HealthCheckFunction;
|
|
67
|
+
/**
|
|
68
|
+
* Create an external service health check
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
73
|
+
* name: 'payment-api',
|
|
74
|
+
* url: 'https://api.payment.com/health',
|
|
75
|
+
* timeout: 5000
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function createExternalServiceHealthCheck(config: {
|
|
80
|
+
name: string;
|
|
81
|
+
url: string;
|
|
82
|
+
timeout?: number;
|
|
83
|
+
}): HealthCheckFunction;
|
|
84
|
+
/**
|
|
85
|
+
* Liveness probe - basic check that service is running
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* app.get('/health/live', livenessProbe());
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function livenessProbe(): (req: any, res: any) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* app.get('/health/ready', readinessProbe({
|
|
99
|
+
* checks: [dbCheck, cacheCheck]
|
|
100
|
+
* }));
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function readinessProbe(config?: {
|
|
104
|
+
checks?: HealthCheckFunction[];
|
|
105
|
+
}): (req: any, res: any) => Promise<void>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Health check middleware for Express applications
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import express from 'express';
|
|
12
|
+
* import { healthCheck } from '@cleocode/lafs-protocol/health';
|
|
13
|
+
*
|
|
14
|
+
* const app = express();
|
|
15
|
+
*
|
|
16
|
+
* // Basic health check
|
|
17
|
+
* app.use('/health', healthCheck());
|
|
18
|
+
*
|
|
19
|
+
* // Custom health checks
|
|
20
|
+
* app.use('/health', healthCheck({
|
|
21
|
+
* checks: [
|
|
22
|
+
* async () => ({
|
|
23
|
+
* name: 'database',
|
|
24
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
25
|
+
* })
|
|
26
|
+
* ]
|
|
27
|
+
* }));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function healthCheck(config = {}) {
|
|
31
|
+
const { path = '/health', checks = [] } = config;
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
return async (req, res) => {
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
const checkResults = [];
|
|
36
|
+
// Run all health checks
|
|
37
|
+
for (const check of checks) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await check();
|
|
41
|
+
result.duration = Date.now() - start;
|
|
42
|
+
checkResults.push(result);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
checkResults.push({
|
|
46
|
+
name: 'unknown',
|
|
47
|
+
status: 'error',
|
|
48
|
+
message: error instanceof Error ? error.message : 'Check failed',
|
|
49
|
+
duration: Date.now() - start
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Add default checks
|
|
54
|
+
checkResults.push({
|
|
55
|
+
name: 'envelopeValidation',
|
|
56
|
+
status: 'ok'
|
|
57
|
+
});
|
|
58
|
+
checkResults.push({
|
|
59
|
+
name: 'tokenBudgets',
|
|
60
|
+
status: 'ok'
|
|
61
|
+
});
|
|
62
|
+
// Determine overall status
|
|
63
|
+
const hasErrors = checkResults.some(c => c.status === 'error');
|
|
64
|
+
const hasWarnings = checkResults.some(c => c.status === 'warning');
|
|
65
|
+
const status = hasErrors
|
|
66
|
+
? 'unhealthy'
|
|
67
|
+
: hasWarnings
|
|
68
|
+
? 'degraded'
|
|
69
|
+
: 'healthy';
|
|
70
|
+
const health = {
|
|
71
|
+
status,
|
|
72
|
+
timestamp,
|
|
73
|
+
version: process.env.npm_package_version || '1.1.0',
|
|
74
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
75
|
+
checks: checkResults
|
|
76
|
+
};
|
|
77
|
+
const statusCode = status === 'healthy' ? 200 : status === 'degraded' ? 200 : 503;
|
|
78
|
+
res.status(statusCode).json(health);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a database health check
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
87
|
+
* checkConnection: async () => await db.ping()
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* app.use('/health', healthCheck({
|
|
91
|
+
* checks: [dbCheck]
|
|
92
|
+
* }));
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function createDatabaseHealthCheck(config) {
|
|
96
|
+
return async () => {
|
|
97
|
+
try {
|
|
98
|
+
const isConnected = await config.checkConnection();
|
|
99
|
+
return {
|
|
100
|
+
name: config.name || 'database',
|
|
101
|
+
status: isConnected ? 'ok' : 'error',
|
|
102
|
+
message: isConnected ? 'Connected' : 'Connection failed'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
name: config.name || 'database',
|
|
108
|
+
status: 'error',
|
|
109
|
+
message: error instanceof Error ? error.message : 'Database check failed'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create an external service health check
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
120
|
+
* name: 'payment-api',
|
|
121
|
+
* url: 'https://api.payment.com/health',
|
|
122
|
+
* timeout: 5000
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function createExternalServiceHealthCheck(config) {
|
|
127
|
+
return async () => {
|
|
128
|
+
const start = Date.now();
|
|
129
|
+
try {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timeout = setTimeout(() => controller.abort(), config.timeout || 5000);
|
|
132
|
+
const response = await fetch(config.url, {
|
|
133
|
+
signal: controller.signal
|
|
134
|
+
});
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
return {
|
|
137
|
+
name: config.name,
|
|
138
|
+
status: response.ok ? 'ok' : 'error',
|
|
139
|
+
message: response.ok ? 'Service healthy' : `HTTP ${response.status}`,
|
|
140
|
+
duration: Date.now() - start
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
name: config.name,
|
|
146
|
+
status: 'error',
|
|
147
|
+
message: error instanceof Error ? error.message : 'Service unreachable',
|
|
148
|
+
duration: Date.now() - start
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Liveness probe - basic check that service is running
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* app.get('/health/live', livenessProbe());
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function livenessProbe() {
|
|
162
|
+
return (req, res) => {
|
|
163
|
+
res.status(200).json({
|
|
164
|
+
status: 'alive',
|
|
165
|
+
timestamp: new Date().toISOString()
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* app.get('/health/ready', readinessProbe({
|
|
175
|
+
* checks: [dbCheck, cacheCheck]
|
|
176
|
+
* }));
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function readinessProbe(config = {}) {
|
|
180
|
+
return async (req, res) => {
|
|
181
|
+
const checkResults = [];
|
|
182
|
+
if (config.checks) {
|
|
183
|
+
for (const check of config.checks) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await check();
|
|
186
|
+
checkResults.push(result);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
checkResults.push({
|
|
190
|
+
name: 'unknown',
|
|
191
|
+
status: 'error',
|
|
192
|
+
message: error instanceof Error ? error.message : 'Check failed'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const hasErrors = checkResults.some(c => c.status === 'error');
|
|
198
|
+
if (hasErrors) {
|
|
199
|
+
res.status(503).json({
|
|
200
|
+
status: 'not ready',
|
|
201
|
+
checks: checkResults
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
res.status(200).json({
|
|
206
|
+
status: 'ready',
|
|
207
|
+
checks: checkResults
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,3 +7,7 @@ export * from "./tokenEstimator.js";
|
|
|
7
7
|
export * from "./budgetEnforcement.js";
|
|
8
8
|
export * from "./mcpAdapter.js";
|
|
9
9
|
export * from "./discovery.js";
|
|
10
|
+
export * from "./health/index.js";
|
|
11
|
+
export * from "./shutdown/index.js";
|
|
12
|
+
export * from "./circuit-breaker/index.js";
|
|
13
|
+
export * from "./a2a/index.js";
|
package/dist/src/index.js
CHANGED
|
@@ -7,3 +7,9 @@ export * from "./tokenEstimator.js";
|
|
|
7
7
|
export * from "./budgetEnforcement.js";
|
|
8
8
|
export * from "./mcpAdapter.js";
|
|
9
9
|
export * from "./discovery.js";
|
|
10
|
+
// Operations & Reliability
|
|
11
|
+
export * from "./health/index.js";
|
|
12
|
+
export * from "./shutdown/index.js";
|
|
13
|
+
export * from "./circuit-breaker/index.js";
|
|
14
|
+
// A2A Integration
|
|
15
|
+
export * from "./a2a/index.js";
|
|
@@ -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
|
+
}
|
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.0.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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/lafs-protocol",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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"
|