nemo-relay-openclaw 0.0.0 → 0.3.0-beta.2
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 +262 -4
- package/dist/index.d.ts +8 -0
- package/dist/index.js +20 -0
- package/dist/openclaw.plugin.json +154 -0
- package/dist/src/config.d.ts +329 -0
- package/dist/src/config.js +185 -0
- package/dist/src/health.d.ts +48 -0
- package/dist/src/health.js +63 -0
- package/dist/src/hook-replay/correlation.d.ts +34 -0
- package/dist/src/hook-replay/correlation.js +40 -0
- package/dist/src/hook-replay/llm.d.ts +52 -0
- package/dist/src/hook-replay/llm.js +1060 -0
- package/dist/src/hook-replay/marks.d.ts +30 -0
- package/dist/src/hook-replay/marks.js +109 -0
- package/dist/src/hook-replay/session.d.ts +157 -0
- package/dist/src/hook-replay/session.js +204 -0
- package/dist/src/hook-replay/tool.d.ts +12 -0
- package/dist/src/hook-replay/tool.js +101 -0
- package/dist/src/hooks-backend.d.ts +94 -0
- package/dist/src/hooks-backend.js +321 -0
- package/dist/src/modules.d.ts +42 -0
- package/dist/src/modules.js +15 -0
- package/dist/src/openclaw-hook-types.d.ts +186 -0
- package/dist/src/openclaw-hook-types.js +3 -0
- package/dist/src/runtime-state.d.ts +66 -0
- package/dist/src/runtime-state.js +420 -0
- package/dist/src/types.d.ts +27 -0
- package/dist/src/types.js +3 -0
- package/index.ts +23 -0
- package/openclaw.plugin.json +154 -0
- package/package.json +62 -24
- package/src/config.ts +251 -0
- package/src/health.ts +116 -0
- package/src/hook-replay/correlation.ts +75 -0
- package/src/hook-replay/llm.ts +1302 -0
- package/src/hook-replay/marks.ts +141 -0
- package/src/hook-replay/session.ts +396 -0
- package/src/hook-replay/tool.ts +142 -0
- package/src/hooks-backend.ts +457 -0
- package/src/modules.ts +79 -0
- package/src/openclaw-hook-types.ts +211 -0
- package/src/runtime-state.ts +553 -0
- package/src/types.ts +34 -0
- package/index.d.ts +0 -2
- package/index.js +0 -15
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
export type BackendKind = 'hooks';
|
|
2
|
+
export type CaptureConfig = {
|
|
3
|
+
includePrompts: boolean;
|
|
4
|
+
includeResponses: boolean;
|
|
5
|
+
stripToolArgs: boolean;
|
|
6
|
+
stripToolResults: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type CorrelationConfig = {
|
|
9
|
+
llmOutputGraceMs: number;
|
|
10
|
+
recordTtlMs: number;
|
|
11
|
+
maxRecordsPerKey: number;
|
|
12
|
+
};
|
|
13
|
+
export type NemoRelayPluginHostConfig = {
|
|
14
|
+
version: number;
|
|
15
|
+
components: unknown[];
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type NemoRelayHookBackendConfig = {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
backend: BackendKind;
|
|
21
|
+
plugins: NemoRelayPluginHostConfig;
|
|
22
|
+
capture: CaptureConfig;
|
|
23
|
+
correlation: CorrelationConfig;
|
|
24
|
+
};
|
|
25
|
+
export declare const NEMO_RELAY_OPENCLAW_JSON_SCHEMA: {
|
|
26
|
+
type: string;
|
|
27
|
+
additionalProperties: boolean;
|
|
28
|
+
properties: {
|
|
29
|
+
enabled: {
|
|
30
|
+
type: string;
|
|
31
|
+
default: boolean;
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
34
|
+
backend: {
|
|
35
|
+
type: string;
|
|
36
|
+
enum: string[];
|
|
37
|
+
default: string;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
plugins: {
|
|
41
|
+
$ref: string;
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
capture: {
|
|
45
|
+
type: string;
|
|
46
|
+
additionalProperties: boolean;
|
|
47
|
+
properties: {
|
|
48
|
+
includePrompts: {
|
|
49
|
+
type: string;
|
|
50
|
+
default: boolean;
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
includeResponses: {
|
|
54
|
+
type: string;
|
|
55
|
+
default: boolean;
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
58
|
+
stripToolArgs: {
|
|
59
|
+
type: string;
|
|
60
|
+
default: boolean;
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
stripToolResults: {
|
|
64
|
+
type: string;
|
|
65
|
+
default: boolean;
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
71
|
+
correlation: {
|
|
72
|
+
type: string;
|
|
73
|
+
additionalProperties: boolean;
|
|
74
|
+
properties: {
|
|
75
|
+
llmOutputGraceMs: {
|
|
76
|
+
type: string;
|
|
77
|
+
minimum: number;
|
|
78
|
+
default: number;
|
|
79
|
+
description: string;
|
|
80
|
+
};
|
|
81
|
+
recordTtlMs: {
|
|
82
|
+
type: string;
|
|
83
|
+
minimum: number;
|
|
84
|
+
default: number;
|
|
85
|
+
description: string;
|
|
86
|
+
};
|
|
87
|
+
maxRecordsPerKey: {
|
|
88
|
+
type: string;
|
|
89
|
+
minimum: number;
|
|
90
|
+
default: number;
|
|
91
|
+
description: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
description: string;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
$defs: {
|
|
98
|
+
nemoRelayPluginConfig: {
|
|
99
|
+
type: string;
|
|
100
|
+
additionalProperties: boolean;
|
|
101
|
+
properties: {
|
|
102
|
+
version: {
|
|
103
|
+
type: string;
|
|
104
|
+
default: number;
|
|
105
|
+
description: string;
|
|
106
|
+
};
|
|
107
|
+
components: {
|
|
108
|
+
type: string;
|
|
109
|
+
items: {
|
|
110
|
+
$ref: string;
|
|
111
|
+
};
|
|
112
|
+
default: never[];
|
|
113
|
+
description: string;
|
|
114
|
+
};
|
|
115
|
+
policy: {
|
|
116
|
+
$ref: string;
|
|
117
|
+
description: string;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
default: {
|
|
121
|
+
version: number;
|
|
122
|
+
components: never[];
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
pluginComponent: {
|
|
126
|
+
type: string;
|
|
127
|
+
required: string[];
|
|
128
|
+
additionalProperties: boolean;
|
|
129
|
+
properties: {
|
|
130
|
+
kind: {
|
|
131
|
+
type: string;
|
|
132
|
+
description: string;
|
|
133
|
+
};
|
|
134
|
+
enabled: {
|
|
135
|
+
type: string;
|
|
136
|
+
default: boolean;
|
|
137
|
+
description: string;
|
|
138
|
+
};
|
|
139
|
+
config: {
|
|
140
|
+
type: string;
|
|
141
|
+
additionalProperties: boolean;
|
|
142
|
+
description: string;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
pluginPolicy: {
|
|
147
|
+
type: string;
|
|
148
|
+
additionalProperties: boolean;
|
|
149
|
+
properties: {
|
|
150
|
+
unknown_component: {
|
|
151
|
+
$ref: string;
|
|
152
|
+
};
|
|
153
|
+
unknown_field: {
|
|
154
|
+
$ref: string;
|
|
155
|
+
};
|
|
156
|
+
unsupported_value: {
|
|
157
|
+
$ref: string;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
unsupportedBehavior: {
|
|
162
|
+
type: string;
|
|
163
|
+
enum: string[];
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
description: string;
|
|
167
|
+
};
|
|
168
|
+
export declare const DEFAULT_CONFIG: NemoRelayHookBackendConfig;
|
|
169
|
+
export declare const nemoRelayConfigSchema: {
|
|
170
|
+
safeParse(value: unknown): {
|
|
171
|
+
success: true;
|
|
172
|
+
data: NemoRelayHookBackendConfig;
|
|
173
|
+
error?: never;
|
|
174
|
+
} | {
|
|
175
|
+
success: false;
|
|
176
|
+
error: {
|
|
177
|
+
issues: {
|
|
178
|
+
path: never[];
|
|
179
|
+
message: string;
|
|
180
|
+
}[];
|
|
181
|
+
};
|
|
182
|
+
data?: never;
|
|
183
|
+
};
|
|
184
|
+
jsonSchema: {
|
|
185
|
+
type: string;
|
|
186
|
+
additionalProperties: boolean;
|
|
187
|
+
properties: {
|
|
188
|
+
enabled: {
|
|
189
|
+
type: string;
|
|
190
|
+
default: boolean;
|
|
191
|
+
description: string;
|
|
192
|
+
};
|
|
193
|
+
backend: {
|
|
194
|
+
type: string;
|
|
195
|
+
enum: string[];
|
|
196
|
+
default: string;
|
|
197
|
+
description: string;
|
|
198
|
+
};
|
|
199
|
+
plugins: {
|
|
200
|
+
$ref: string;
|
|
201
|
+
description: string;
|
|
202
|
+
};
|
|
203
|
+
capture: {
|
|
204
|
+
type: string;
|
|
205
|
+
additionalProperties: boolean;
|
|
206
|
+
properties: {
|
|
207
|
+
includePrompts: {
|
|
208
|
+
type: string;
|
|
209
|
+
default: boolean;
|
|
210
|
+
description: string;
|
|
211
|
+
};
|
|
212
|
+
includeResponses: {
|
|
213
|
+
type: string;
|
|
214
|
+
default: boolean;
|
|
215
|
+
description: string;
|
|
216
|
+
};
|
|
217
|
+
stripToolArgs: {
|
|
218
|
+
type: string;
|
|
219
|
+
default: boolean;
|
|
220
|
+
description: string;
|
|
221
|
+
};
|
|
222
|
+
stripToolResults: {
|
|
223
|
+
type: string;
|
|
224
|
+
default: boolean;
|
|
225
|
+
description: string;
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
description: string;
|
|
229
|
+
};
|
|
230
|
+
correlation: {
|
|
231
|
+
type: string;
|
|
232
|
+
additionalProperties: boolean;
|
|
233
|
+
properties: {
|
|
234
|
+
llmOutputGraceMs: {
|
|
235
|
+
type: string;
|
|
236
|
+
minimum: number;
|
|
237
|
+
default: number;
|
|
238
|
+
description: string;
|
|
239
|
+
};
|
|
240
|
+
recordTtlMs: {
|
|
241
|
+
type: string;
|
|
242
|
+
minimum: number;
|
|
243
|
+
default: number;
|
|
244
|
+
description: string;
|
|
245
|
+
};
|
|
246
|
+
maxRecordsPerKey: {
|
|
247
|
+
type: string;
|
|
248
|
+
minimum: number;
|
|
249
|
+
default: number;
|
|
250
|
+
description: string;
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
description: string;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
$defs: {
|
|
257
|
+
nemoRelayPluginConfig: {
|
|
258
|
+
type: string;
|
|
259
|
+
additionalProperties: boolean;
|
|
260
|
+
properties: {
|
|
261
|
+
version: {
|
|
262
|
+
type: string;
|
|
263
|
+
default: number;
|
|
264
|
+
description: string;
|
|
265
|
+
};
|
|
266
|
+
components: {
|
|
267
|
+
type: string;
|
|
268
|
+
items: {
|
|
269
|
+
$ref: string;
|
|
270
|
+
};
|
|
271
|
+
default: never[];
|
|
272
|
+
description: string;
|
|
273
|
+
};
|
|
274
|
+
policy: {
|
|
275
|
+
$ref: string;
|
|
276
|
+
description: string;
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
default: {
|
|
280
|
+
version: number;
|
|
281
|
+
components: never[];
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
pluginComponent: {
|
|
285
|
+
type: string;
|
|
286
|
+
required: string[];
|
|
287
|
+
additionalProperties: boolean;
|
|
288
|
+
properties: {
|
|
289
|
+
kind: {
|
|
290
|
+
type: string;
|
|
291
|
+
description: string;
|
|
292
|
+
};
|
|
293
|
+
enabled: {
|
|
294
|
+
type: string;
|
|
295
|
+
default: boolean;
|
|
296
|
+
description: string;
|
|
297
|
+
};
|
|
298
|
+
config: {
|
|
299
|
+
type: string;
|
|
300
|
+
additionalProperties: boolean;
|
|
301
|
+
description: string;
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
pluginPolicy: {
|
|
306
|
+
type: string;
|
|
307
|
+
additionalProperties: boolean;
|
|
308
|
+
properties: {
|
|
309
|
+
unknown_component: {
|
|
310
|
+
$ref: string;
|
|
311
|
+
};
|
|
312
|
+
unknown_field: {
|
|
313
|
+
$ref: string;
|
|
314
|
+
};
|
|
315
|
+
unsupported_value: {
|
|
316
|
+
$ref: string;
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
unsupportedBehavior: {
|
|
321
|
+
type: string;
|
|
322
|
+
enum: string[];
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
description: string;
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
/** Parse OpenClaw plugin JSON into the normalized hook backend config. */
|
|
329
|
+
export declare function parseConfig(value: unknown): NemoRelayHookBackendConfig;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import manifest from '../openclaw.plugin.json' with { type: 'json' };
|
|
4
|
+
const DEFAULT_PLUGIN_HOST_CONFIG = {
|
|
5
|
+
version: 1,
|
|
6
|
+
components: [],
|
|
7
|
+
};
|
|
8
|
+
export const NEMO_RELAY_OPENCLAW_JSON_SCHEMA = manifest.configSchema;
|
|
9
|
+
export const DEFAULT_CONFIG = {
|
|
10
|
+
enabled: true,
|
|
11
|
+
backend: 'hooks',
|
|
12
|
+
plugins: DEFAULT_PLUGIN_HOST_CONFIG,
|
|
13
|
+
capture: {
|
|
14
|
+
includePrompts: true,
|
|
15
|
+
includeResponses: true,
|
|
16
|
+
stripToolArgs: true,
|
|
17
|
+
stripToolResults: true,
|
|
18
|
+
},
|
|
19
|
+
correlation: {
|
|
20
|
+
llmOutputGraceMs: 250,
|
|
21
|
+
recordTtlMs: 600_000,
|
|
22
|
+
maxRecordsPerKey: 32,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export const nemoRelayConfigSchema = {
|
|
26
|
+
safeParse(value) {
|
|
27
|
+
try {
|
|
28
|
+
return { success: true, data: parseConfig(value) };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: {
|
|
34
|
+
issues: [
|
|
35
|
+
{
|
|
36
|
+
path: [],
|
|
37
|
+
message: error instanceof Error ? error.message : String(error),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
jsonSchema: NEMO_RELAY_OPENCLAW_JSON_SCHEMA,
|
|
45
|
+
};
|
|
46
|
+
/** Parse OpenClaw plugin JSON into the normalized hook backend config. */
|
|
47
|
+
export function parseConfig(value) {
|
|
48
|
+
const raw = asRecord(value, 'config', true);
|
|
49
|
+
rejectRemovedFields(raw);
|
|
50
|
+
rejectUnknownFields(raw, 'config', ['enabled', 'backend', 'plugins', 'capture', 'correlation']);
|
|
51
|
+
const backend = optionalString(raw.backend, 'backend') ?? DEFAULT_CONFIG.backend;
|
|
52
|
+
if (backend !== 'hooks') {
|
|
53
|
+
throw new Error(`unsupported nemo-relay backend: ${backend}`);
|
|
54
|
+
}
|
|
55
|
+
const capture = asRecord(raw.capture, 'capture', true);
|
|
56
|
+
const correlation = asRecord(raw.correlation, 'correlation', true);
|
|
57
|
+
return {
|
|
58
|
+
enabled: optionalBoolean(raw.enabled, 'enabled') ?? DEFAULT_CONFIG.enabled,
|
|
59
|
+
backend,
|
|
60
|
+
plugins: parsePluginHostConfig(raw.plugins),
|
|
61
|
+
capture: {
|
|
62
|
+
includePrompts: optionalBoolean(capture.includePrompts, 'capture.includePrompts') ?? DEFAULT_CONFIG.capture.includePrompts,
|
|
63
|
+
includeResponses: optionalBoolean(capture.includeResponses, 'capture.includeResponses') ??
|
|
64
|
+
DEFAULT_CONFIG.capture.includeResponses,
|
|
65
|
+
stripToolArgs: optionalBoolean(capture.stripToolArgs, 'capture.stripToolArgs') ?? DEFAULT_CONFIG.capture.stripToolArgs,
|
|
66
|
+
stripToolResults: optionalBoolean(capture.stripToolResults, 'capture.stripToolResults') ??
|
|
67
|
+
DEFAULT_CONFIG.capture.stripToolResults,
|
|
68
|
+
},
|
|
69
|
+
correlation: {
|
|
70
|
+
llmOutputGraceMs: optionalNonNegativeInteger(correlation.llmOutputGraceMs, 'correlation.llmOutputGraceMs') ??
|
|
71
|
+
DEFAULT_CONFIG.correlation.llmOutputGraceMs,
|
|
72
|
+
recordTtlMs: optionalNonNegativeInteger(correlation.recordTtlMs, 'correlation.recordTtlMs') ??
|
|
73
|
+
DEFAULT_CONFIG.correlation.recordTtlMs,
|
|
74
|
+
maxRecordsPerKey: optionalPositiveInteger(correlation.maxRecordsPerKey, 'correlation.maxRecordsPerKey') ??
|
|
75
|
+
DEFAULT_CONFIG.correlation.maxRecordsPerKey,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Normalize the optional generic NeMo Relay plugin-host config embedded in OpenClaw config. */
|
|
80
|
+
function parsePluginHostConfig(value) {
|
|
81
|
+
if (value === undefined) {
|
|
82
|
+
return clonePluginHostConfig(DEFAULT_PLUGIN_HOST_CONFIG);
|
|
83
|
+
}
|
|
84
|
+
const record = asRecord(value, 'plugins', false);
|
|
85
|
+
const version = optionalNumber(record.version, 'plugins.version') ?? 1;
|
|
86
|
+
const components = record.components === undefined ? [] : record.components;
|
|
87
|
+
if (!Array.isArray(components)) {
|
|
88
|
+
throw new Error('plugins.components must be an array');
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...record,
|
|
92
|
+
version,
|
|
93
|
+
components: [...components],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Clone the mutable plugin-host component list before putting it in runtime state. */
|
|
97
|
+
function clonePluginHostConfig(config) {
|
|
98
|
+
return {
|
|
99
|
+
...config,
|
|
100
|
+
components: [...config.components],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Require an object config section, optionally treating undefined as an empty object. */
|
|
104
|
+
function asRecord(value, path, optional) {
|
|
105
|
+
if (value === undefined && optional) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`${path} must be an object`);
|
|
112
|
+
}
|
|
113
|
+
/** Reject config fields removed by the generic plugin-host pivot with direct migration hints. */
|
|
114
|
+
function rejectRemovedFields(raw) {
|
|
115
|
+
if (raw.nemoRelay !== undefined) {
|
|
116
|
+
throw new Error('nemoRelay.pluginConfig was removed; use top-level plugins instead');
|
|
117
|
+
}
|
|
118
|
+
if (raw.atif !== undefined) {
|
|
119
|
+
throw new Error('atif was removed; configure plugins.components[].config.atif on the observability component');
|
|
120
|
+
}
|
|
121
|
+
if (raw.telemetry !== undefined) {
|
|
122
|
+
throw new Error('telemetry was removed; configure plugins.components[].config.opentelemetry or openinference on the observability component');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Keep parser behavior aligned with the manifest's additionalProperties=false contract. */
|
|
126
|
+
function rejectUnknownFields(raw, path, allowed) {
|
|
127
|
+
const allowedSet = new Set(allowed);
|
|
128
|
+
for (const key of Object.keys(raw)) {
|
|
129
|
+
if (!allowedSet.has(key)) {
|
|
130
|
+
throw new Error(`${path}.${key} is not supported`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Parse an optional boolean while producing config-path-specific error messages. */
|
|
135
|
+
function optionalBoolean(value, path) {
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (typeof value !== 'boolean') {
|
|
140
|
+
throw new Error(`${path} must be a boolean`);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
/** Parse an optional finite number while preserving undefined for default fallback. */
|
|
145
|
+
function optionalNumber(value, path) {
|
|
146
|
+
if (value === undefined) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
150
|
+
throw new Error(`${path} must be a finite number`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
/** Parse an optional integer where zero is valid, such as timeouts. */
|
|
155
|
+
function optionalNonNegativeInteger(value, path) {
|
|
156
|
+
const parsed = optionalNumber(value, path);
|
|
157
|
+
if (parsed === undefined) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
161
|
+
throw new Error(`${path} must be a non-negative integer`);
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
/** Parse an optional integer where zero would disable required bounded storage. */
|
|
166
|
+
function optionalPositiveInteger(value, path) {
|
|
167
|
+
const parsed = optionalNumber(value, path);
|
|
168
|
+
if (parsed === undefined) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
172
|
+
throw new Error(`${path} must be a positive integer`);
|
|
173
|
+
}
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
/** Parse an optional string while rejecting accidental non-string config values. */
|
|
177
|
+
function optionalString(value, path) {
|
|
178
|
+
if (value === undefined) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if (typeof value !== 'string') {
|
|
182
|
+
throw new Error(`${path} must be a string`);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health snapshot construction for the plugin gateway status method.
|
|
3
|
+
*
|
|
4
|
+
* Runtime state owns status transitions; this file turns that state into a
|
|
5
|
+
* stable, JSON-friendly status payload for operators and tests.
|
|
6
|
+
*/
|
|
7
|
+
import type { NemoRelayHookBackendConfig } from './config.js';
|
|
8
|
+
import type { HookReplayBackendState } from './hook-replay/session.js';
|
|
9
|
+
export type HookReplayBackendStatus = {
|
|
10
|
+
state: 'not_initialized';
|
|
11
|
+
reason?: string;
|
|
12
|
+
} | {
|
|
13
|
+
state: 'disabled';
|
|
14
|
+
reason?: string;
|
|
15
|
+
} | {
|
|
16
|
+
state: 'ready';
|
|
17
|
+
} | {
|
|
18
|
+
state: 'degraded';
|
|
19
|
+
reason: string;
|
|
20
|
+
} | {
|
|
21
|
+
state: 'stopping';
|
|
22
|
+
} | {
|
|
23
|
+
state: 'stopped';
|
|
24
|
+
reason?: string;
|
|
25
|
+
};
|
|
26
|
+
export type OutputHealthState = 'enabled' | 'disabled' | 'degraded';
|
|
27
|
+
export type NemoRelayHealthSnapshot = {
|
|
28
|
+
id: 'nemo-relay';
|
|
29
|
+
backend: 'hooks';
|
|
30
|
+
status: HookReplayBackendStatus;
|
|
31
|
+
initializedPluginHost: boolean;
|
|
32
|
+
state: HookReplayBackendStatus['state'];
|
|
33
|
+
outputs: {
|
|
34
|
+
atif: OutputHealthState;
|
|
35
|
+
otel: OutputHealthState;
|
|
36
|
+
openInference: OutputHealthState;
|
|
37
|
+
};
|
|
38
|
+
counters: HookReplayBackendState['counters'];
|
|
39
|
+
lastError?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Build a complete health payload from runtime status, configured outputs, and counters. */
|
|
42
|
+
export declare function createHealthSnapshot(params: {
|
|
43
|
+
status: HookReplayBackendStatus;
|
|
44
|
+
initializedPluginHost: boolean;
|
|
45
|
+
pluginHostOutputsHealthy: boolean;
|
|
46
|
+
config: NemoRelayHookBackendConfig;
|
|
47
|
+
counters?: HookReplayBackendState['counters'];
|
|
48
|
+
}): NemoRelayHealthSnapshot;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/** Build a complete health payload from runtime status, configured outputs, and counters. */
|
|
4
|
+
export function createHealthSnapshot(params) {
|
|
5
|
+
const lastError = 'reason' in params.status ? params.status.reason : undefined;
|
|
6
|
+
const outputs = configuredObservabilityOutputs(params.config);
|
|
7
|
+
const pluginHostFailed = params.status.state === 'degraded' && !params.pluginHostOutputsHealthy;
|
|
8
|
+
return {
|
|
9
|
+
id: 'nemo-relay',
|
|
10
|
+
backend: 'hooks',
|
|
11
|
+
status: params.status,
|
|
12
|
+
initializedPluginHost: params.initializedPluginHost,
|
|
13
|
+
state: params.status.state,
|
|
14
|
+
outputs: {
|
|
15
|
+
atif: outputHealth(outputs.atif, pluginHostFailed),
|
|
16
|
+
otel: outputHealth(outputs.otel, pluginHostFailed),
|
|
17
|
+
openInference: outputHealth(outputs.openInference, pluginHostFailed),
|
|
18
|
+
},
|
|
19
|
+
counters: params.counters ?? emptyCounters(),
|
|
20
|
+
...(lastError === undefined ? {} : { lastError }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function outputHealth(enabled, pluginHostFailed) {
|
|
24
|
+
if (!enabled) {
|
|
25
|
+
return 'disabled';
|
|
26
|
+
}
|
|
27
|
+
return pluginHostFailed ? 'degraded' : 'enabled';
|
|
28
|
+
}
|
|
29
|
+
/** Inspect generic PluginConfig components for configured observability outputs. */
|
|
30
|
+
function configuredObservabilityOutputs(config) {
|
|
31
|
+
const outputs = { atif: false, otel: false, openInference: false };
|
|
32
|
+
for (const component of config.plugins.components) {
|
|
33
|
+
const record = asRecord(component);
|
|
34
|
+
if (record?.kind !== 'observability' || record.enabled === false) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const componentConfig = asRecord(record.config);
|
|
38
|
+
outputs.atif ||= sectionEnabled(componentConfig?.atif);
|
|
39
|
+
outputs.otel ||= sectionEnabled(componentConfig?.opentelemetry);
|
|
40
|
+
outputs.openInference ||= sectionEnabled(componentConfig?.openinference);
|
|
41
|
+
}
|
|
42
|
+
return outputs;
|
|
43
|
+
}
|
|
44
|
+
function sectionEnabled(value) {
|
|
45
|
+
const section = asRecord(value);
|
|
46
|
+
return section?.enabled === true;
|
|
47
|
+
}
|
|
48
|
+
function asRecord(value) {
|
|
49
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/** Provide zero counters before hook replay has initialized. */
|
|
55
|
+
function emptyCounters() {
|
|
56
|
+
return {
|
|
57
|
+
llmSpansReplayed: 0,
|
|
58
|
+
toolSpansReplayed: 0,
|
|
59
|
+
marksEmitted: 0,
|
|
60
|
+
replayErrors: 0,
|
|
61
|
+
skippedEvents: 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation-key and timestamp utilities for hook replay.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw emits request, response, timing, and tool events separately. These
|
|
5
|
+
* helpers keep key construction consistent across the replay modules.
|
|
6
|
+
*/
|
|
7
|
+
export type LlmKeyInput = {
|
|
8
|
+
sessionId?: string | undefined;
|
|
9
|
+
runId?: string | undefined;
|
|
10
|
+
provider?: string | undefined;
|
|
11
|
+
model?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
export type ModelTimingKeyInput = {
|
|
14
|
+
runId: string;
|
|
15
|
+
callId: string;
|
|
16
|
+
};
|
|
17
|
+
export type TimestampedRecord = {
|
|
18
|
+
observedAtMs?: number | undefined;
|
|
19
|
+
startedAtMs?: number | undefined;
|
|
20
|
+
endedAtMs?: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
/** Serialize correlation tuple parts while preserving empty or missing fields as null. */
|
|
23
|
+
export declare function tupleKey(parts: unknown[]): string;
|
|
24
|
+
/** Build the best available key for pairing public llm_input and llm_output hooks. */
|
|
25
|
+
export declare function llmKey(input: LlmKeyInput): string;
|
|
26
|
+
/** Build the stronger key for model timing events that include a provider call id. */
|
|
27
|
+
export declare function modelTimingKey(input: ModelTimingKeyInput): string;
|
|
28
|
+
export declare const modelTimingLlmKey: typeof llmKey;
|
|
29
|
+
/** Remove correlation records that are too old to safely pair with later events. */
|
|
30
|
+
export declare function evictExpiredRecords<T extends TimestampedRecord>(map: Map<string, T[]>, nowMs: number, ttlMs: number): void;
|
|
31
|
+
/** Return wall-clock microseconds for NeMo Relay span APIs. */
|
|
32
|
+
export declare function nowMicros(): number;
|
|
33
|
+
/** Infer a start timestamp when OpenClaw gives end time plus duration. */
|
|
34
|
+
export declare function startMicrosFromDuration(endMicros: number, durationMs: number | undefined): number | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/** Serialize correlation tuple parts while preserving empty or missing fields as null. */
|
|
4
|
+
export function tupleKey(parts) {
|
|
5
|
+
return JSON.stringify(parts.map((part) => (typeof part === 'string' && part.length > 0 ? part : null)));
|
|
6
|
+
}
|
|
7
|
+
/** Build the best available key for pairing public llm_input and llm_output hooks. */
|
|
8
|
+
export function llmKey(input) {
|
|
9
|
+
return tupleKey([input.sessionId, input.runId, input.provider, input.model]);
|
|
10
|
+
}
|
|
11
|
+
/** Build the stronger key for model timing events that include a provider call id. */
|
|
12
|
+
export function modelTimingKey(input) {
|
|
13
|
+
return tupleKey([input.runId, input.callId]);
|
|
14
|
+
}
|
|
15
|
+
// Model timing fallback uses the same provider/model tuple as LLM replay.
|
|
16
|
+
export const modelTimingLlmKey = llmKey;
|
|
17
|
+
/** Remove correlation records that are too old to safely pair with later events. */
|
|
18
|
+
export function evictExpiredRecords(map, nowMs, ttlMs) {
|
|
19
|
+
for (const [key, records] of map) {
|
|
20
|
+
const retained = records.filter((record) => nowMs - recordTimestamp(record) <= ttlMs);
|
|
21
|
+
if (retained.length === 0) {
|
|
22
|
+
map.delete(key);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
map.set(key, retained);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Return wall-clock microseconds for NeMo Relay span APIs. */
|
|
30
|
+
export function nowMicros() {
|
|
31
|
+
return Date.now() * 1000;
|
|
32
|
+
}
|
|
33
|
+
/** Infer a start timestamp when OpenClaw gives end time plus duration. */
|
|
34
|
+
export function startMicrosFromDuration(endMicros, durationMs) {
|
|
35
|
+
return durationMs === undefined ? null : endMicros - Math.round(durationMs * 1000);
|
|
36
|
+
}
|
|
37
|
+
/** Select the most recent timestamp field available on a correlation record. */
|
|
38
|
+
function recordTimestamp(record) {
|
|
39
|
+
return record.observedAtMs ?? record.endedAtMs ?? record.startedAtMs ?? 0;
|
|
40
|
+
}
|