nemo-relay-openclaw 0.3.0-beta.2 → 0.3.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 +9 -9
- package/dist/src/hook-replay/llm.js +24 -2
- package/dist/src/hooks-backend.d.ts +2 -0
- package/dist/src/hooks-backend.js +10 -0
- package/dist/src/modules.d.ts +1 -0
- package/package.json +7 -7
- package/src/hook-replay/llm.ts +29 -2
- package/src/hooks-backend.ts +13 -0
- package/src/modules.ts +1 -0
package/README.md
CHANGED
|
@@ -12,10 +12,11 @@ observability component can export as ATIF JSON, OpenTelemetry spans, and
|
|
|
12
12
|
OpenInference/Phoenix spans. The same generic plugin config path can initialize
|
|
13
13
|
Adaptive components for hook-backed telemetry learning.
|
|
14
14
|
|
|
15
|
-
This public OpenClaw plugin package uses OpenClaw public hooks. It
|
|
16
|
-
|
|
17
|
-
requests. For middleware-backed
|
|
18
|
-
patch-based OpenClaw integration from
|
|
15
|
+
This public OpenClaw plugin package uses OpenClaw public hooks. It can run
|
|
16
|
+
pre-tool conditional guardrails when OpenClaw invokes the before-tool hook, but
|
|
17
|
+
it does not rewrite provider routing or model requests. For middleware-backed
|
|
18
|
+
behavior that changes execution, use the patch-based OpenClaw integration from
|
|
19
|
+
the NeMo Relay repository.
|
|
19
20
|
|
|
20
21
|
## Why Use It?
|
|
21
22
|
|
|
@@ -175,7 +176,7 @@ do not use, or set their `enabled` fields to `false`.
|
|
|
175
176
|
Fields inside `config.plugins` are NeMo Relay generic plugin configuration, so
|
|
176
177
|
they use `snake_case` regardless of language. For the full exporter field list,
|
|
177
178
|
see the NeMo Relay Observability Plugin schema in the top-level NeMo Relay
|
|
178
|
-
documentation at [nvidia.
|
|
179
|
+
documentation at [docs.nvidia.com/nemo/relay](https://docs.nvidia.com/nemo/relay).
|
|
179
180
|
|
|
180
181
|
## Verify the Integration
|
|
181
182
|
|
|
@@ -204,11 +205,10 @@ openclaw gateway call nemoRelay.status --json
|
|
|
204
205
|
|
|
205
206
|
## Current Limits
|
|
206
207
|
|
|
207
|
-
The plugin maps supported OpenClaw hook events into NeMo Relay telemetry
|
|
208
|
-
|
|
208
|
+
The plugin maps supported OpenClaw hook events into NeMo Relay telemetry and can
|
|
209
|
+
run pre-tool conditional guardrail checks.
|
|
209
210
|
|
|
210
|
-
It does not
|
|
211
|
-
or provider request payloads.
|
|
211
|
+
It does not rewrite provider routing or provider request payloads.
|
|
212
212
|
|
|
213
213
|
Current OpenClaw public hooks expose request, response, message-write, and
|
|
214
214
|
provider timing details through separate event streams. The plugin correlates
|
|
@@ -324,12 +324,14 @@ export function buildReplayLlmRequest(input, output, config, source = 'openclaw.
|
|
|
324
324
|
/** Build the response payload passed to NeMo Relay for a replayed LLM span. */
|
|
325
325
|
export function buildReplayLlmResponse(event, timing, config) {
|
|
326
326
|
const usage = mapUsage(event.usage);
|
|
327
|
-
const
|
|
327
|
+
const assistantToolCalls = assistantToolCallsForOutput(event, config);
|
|
328
|
+
const assistantToolCallNames = toolCallNames(assistantToolCalls);
|
|
328
329
|
return toJsonValue({
|
|
329
330
|
role: 'assistant',
|
|
330
331
|
content: config.capture.includeResponses
|
|
331
332
|
? responseContent(event.assistantTexts, assistantToolCallNames)
|
|
332
333
|
: undefined,
|
|
334
|
+
tool_calls: assistantToolCalls.length > 0 ? assistantToolCalls : undefined,
|
|
333
335
|
assistant_texts_count: event.assistantTexts.length,
|
|
334
336
|
resolved_ref: event.resolvedRef,
|
|
335
337
|
harness_id: event.harnessId,
|
|
@@ -347,6 +349,13 @@ export function buildReplayLlmResponse(event, timing, config) {
|
|
|
347
349
|
},
|
|
348
350
|
});
|
|
349
351
|
}
|
|
352
|
+
/** Return assistant tool calls from the current OpenClaw llm_output contract. */
|
|
353
|
+
function assistantToolCallsForOutput(event, config) {
|
|
354
|
+
const explicit = Array.isArray(event.assistantToolCalls) ? event.assistantToolCalls : [];
|
|
355
|
+
const inferred = explicit.length > 0 || !isRecord(event.lastAssistant) ? explicit : extractToolCalls(event.lastAssistant);
|
|
356
|
+
const snapshot = snapshotMessages(inferred);
|
|
357
|
+
return config.capture.stripToolArgs ? snapshot.map(stripToolCallArgs) : snapshot;
|
|
358
|
+
}
|
|
350
359
|
/** Replay an output whose matching input never arrived before the grace timeout. */
|
|
351
360
|
function replayExpiredPendingOutput(manager, key, record) {
|
|
352
361
|
try {
|
|
@@ -658,6 +667,9 @@ function stripToolCallArgs(value) {
|
|
|
658
667
|
stripped[key] = { stripped: true };
|
|
659
668
|
}
|
|
660
669
|
}
|
|
670
|
+
if (isRecord(stripped.function) && stripped.function.arguments !== undefined) {
|
|
671
|
+
stripped.function = { ...stripped.function, arguments: { stripped: true } };
|
|
672
|
+
}
|
|
661
673
|
return stripped;
|
|
662
674
|
}
|
|
663
675
|
/** Strip provider thinking/signature payloads that are noisy in trace UIs. */
|
|
@@ -761,6 +773,8 @@ function extractToolCalls(message) {
|
|
|
761
773
|
function isToolCallLike(value) {
|
|
762
774
|
return (isRecord(value) &&
|
|
763
775
|
(value.type === 'toolCall' ||
|
|
776
|
+
value.type === 'toolcall' ||
|
|
777
|
+
value.type === 'function_call' ||
|
|
764
778
|
value.type === 'tool_use' ||
|
|
765
779
|
value.type === 'tool-call' ||
|
|
766
780
|
value.toolName !== undefined ||
|
|
@@ -776,13 +790,21 @@ function toolCallNames(toolCalls) {
|
|
|
776
790
|
if (!isRecord(toolCall)) {
|
|
777
791
|
continue;
|
|
778
792
|
}
|
|
779
|
-
const name = stringField(toolCall, 'name') ??
|
|
793
|
+
const name = stringField(toolCall, 'name') ??
|
|
794
|
+
stringField(toolCall, 'toolName') ??
|
|
795
|
+
stringField(toolCall, 'functionName') ??
|
|
796
|
+
functionToolCallName(toolCall);
|
|
780
797
|
if (name) {
|
|
781
798
|
names.push(name);
|
|
782
799
|
}
|
|
783
800
|
}
|
|
784
801
|
return names;
|
|
785
802
|
}
|
|
803
|
+
/** Read OpenAI-style nested function tool-call names. */
|
|
804
|
+
function functionToolCallName(toolCall) {
|
|
805
|
+
const fn = toolCall.function;
|
|
806
|
+
return isRecord(fn) ? stringField(fn, 'name') : undefined;
|
|
807
|
+
}
|
|
786
808
|
/** Convert stored message-write usage back into the llm_output usage contract. */
|
|
787
809
|
function mapHookUsage(usage) {
|
|
788
810
|
const mapped = mapUsage(usage);
|
|
@@ -84,6 +84,8 @@ export declare class HookReplayBackend {
|
|
|
84
84
|
private emitSessionMark;
|
|
85
85
|
/** Close every active session with the same lifecycle summary. */
|
|
86
86
|
private closeAllSessions;
|
|
87
|
+
/** Wait for native subscriber/exporter delivery after a replay closure boundary. */
|
|
88
|
+
private flushSubscriberDelivery;
|
|
87
89
|
/** Build the narrow manager interface consumed by focused replay modules. */
|
|
88
90
|
private sessionManager;
|
|
89
91
|
/** Log one warning per key to avoid noisy repeated hook failures. */
|
|
@@ -253,6 +253,7 @@ export class HookReplayBackend {
|
|
|
253
253
|
async closeSession(session, summary) {
|
|
254
254
|
drainSession(this.sessionManager(), session);
|
|
255
255
|
closeSessionRoot(this.sessionManager(), session, summary, session.finalOutput ?? summary);
|
|
256
|
+
this.flushSubscriberDelivery('session_close');
|
|
256
257
|
deleteSession(this.stateValue, session);
|
|
257
258
|
}
|
|
258
259
|
/** Emit a session-level OpenClaw lifecycle mark. */
|
|
@@ -273,6 +274,15 @@ export class HookReplayBackend {
|
|
|
273
274
|
await this.closeSession(session, summary);
|
|
274
275
|
}
|
|
275
276
|
}
|
|
277
|
+
/** Wait for native subscriber/exporter delivery after a replay closure boundary. */
|
|
278
|
+
flushSubscriberDelivery(label) {
|
|
279
|
+
try {
|
|
280
|
+
this.nf.flushSubscribers?.();
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
this.logBoundedWarn(`flush-subscribers:${label}`, `nemo-relay subscriber flush failed: label=${label} error=${toMessage(error)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
276
286
|
/** Build the narrow manager interface consumed by focused replay modules. */
|
|
277
287
|
sessionManager() {
|
|
278
288
|
return {
|
package/dist/src/modules.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type NemoRelayRuntimeModule = Omit<Pick<typeof NemoRelayRuntime, NemoRela
|
|
|
20
20
|
ScopeType: {
|
|
21
21
|
Agent?: Parameters<typeof NemoRelayRuntime.pushScope>[1];
|
|
22
22
|
} | undefined;
|
|
23
|
+
flushSubscribers?: typeof NemoRelayRuntime.flushSubscribers;
|
|
23
24
|
};
|
|
24
25
|
/**
|
|
25
26
|
* @internal Package-owned subset of the dynamically imported
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nemo-relay-openclaw",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "NeMo Relay-authored observability plugin for OpenClaw.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"./dist/index.js"
|
|
43
43
|
],
|
|
44
44
|
"compat": {
|
|
45
|
-
"pluginApi": ">=2026.5.
|
|
46
|
-
"minGatewayVersion": "2026.5.
|
|
45
|
+
"pluginApi": ">=2026.5.26",
|
|
46
|
+
"minGatewayVersion": "2026.5.26"
|
|
47
47
|
},
|
|
48
48
|
"build": {
|
|
49
|
-
"openclawVersion": "2026.5.
|
|
50
|
-
"pluginSdkVersion": "2026.5.
|
|
49
|
+
"openclawVersion": "2026.5.26",
|
|
50
|
+
"pluginSdkVersion": "2026.5.26"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"test:live": "npm run build:test && node scripts/test-live.mjs"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"openclaw": "
|
|
63
|
+
"openclaw": "^2026.5.26"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"nemo-relay-node": "0.3.0
|
|
66
|
+
"nemo-relay-node": "0.3.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/node": "^24.0.0",
|
package/src/hook-replay/llm.ts
CHANGED
|
@@ -451,12 +451,14 @@ export function buildReplayLlmResponse(
|
|
|
451
451
|
config: NemoRelayHookBackendConfig,
|
|
452
452
|
): JsonValue {
|
|
453
453
|
const usage = mapUsage(event.usage);
|
|
454
|
-
const
|
|
454
|
+
const assistantToolCalls = assistantToolCallsForOutput(event, config);
|
|
455
|
+
const assistantToolCallNames = toolCallNames(assistantToolCalls);
|
|
455
456
|
return toJsonValue({
|
|
456
457
|
role: 'assistant',
|
|
457
458
|
content: config.capture.includeResponses
|
|
458
459
|
? responseContent(event.assistantTexts, assistantToolCallNames)
|
|
459
460
|
: undefined,
|
|
461
|
+
tool_calls: assistantToolCalls.length > 0 ? assistantToolCalls : undefined,
|
|
460
462
|
assistant_texts_count: event.assistantTexts.length,
|
|
461
463
|
resolved_ref: event.resolvedRef,
|
|
462
464
|
harness_id: event.harnessId,
|
|
@@ -475,6 +477,17 @@ export function buildReplayLlmResponse(
|
|
|
475
477
|
});
|
|
476
478
|
}
|
|
477
479
|
|
|
480
|
+
/** Return assistant tool calls from the current OpenClaw llm_output contract. */
|
|
481
|
+
function assistantToolCallsForOutput(
|
|
482
|
+
event: PluginHookLlmOutputEvent,
|
|
483
|
+
config: NemoRelayHookBackendConfig,
|
|
484
|
+
): unknown[] {
|
|
485
|
+
const explicit = Array.isArray(event.assistantToolCalls) ? event.assistantToolCalls : [];
|
|
486
|
+
const inferred = explicit.length > 0 || !isRecord(event.lastAssistant) ? explicit : extractToolCalls(event.lastAssistant);
|
|
487
|
+
const snapshot = snapshotMessages(inferred);
|
|
488
|
+
return config.capture.stripToolArgs ? snapshot.map(stripToolCallArgs) : snapshot;
|
|
489
|
+
}
|
|
490
|
+
|
|
478
491
|
/** Replay an output whose matching input never arrived before the grace timeout. */
|
|
479
492
|
function replayExpiredPendingOutput(manager: SessionManager, key: string, record: PendingLlmOutputRecord): void {
|
|
480
493
|
try {
|
|
@@ -855,6 +868,9 @@ function stripToolCallArgs(value: unknown): unknown {
|
|
|
855
868
|
stripped[key] = { stripped: true };
|
|
856
869
|
}
|
|
857
870
|
}
|
|
871
|
+
if (isRecord(stripped.function) && stripped.function.arguments !== undefined) {
|
|
872
|
+
stripped.function = { ...stripped.function, arguments: { stripped: true } };
|
|
873
|
+
}
|
|
858
874
|
return stripped;
|
|
859
875
|
}
|
|
860
876
|
|
|
@@ -969,6 +985,8 @@ function isToolCallLike(value: unknown): boolean {
|
|
|
969
985
|
return (
|
|
970
986
|
isRecord(value) &&
|
|
971
987
|
(value.type === 'toolCall' ||
|
|
988
|
+
value.type === 'toolcall' ||
|
|
989
|
+
value.type === 'function_call' ||
|
|
972
990
|
value.type === 'tool_use' ||
|
|
973
991
|
value.type === 'tool-call' ||
|
|
974
992
|
value.toolName !== undefined ||
|
|
@@ -987,7 +1005,10 @@ function toolCallNames(toolCalls: unknown[] | undefined): string[] {
|
|
|
987
1005
|
continue;
|
|
988
1006
|
}
|
|
989
1007
|
const name =
|
|
990
|
-
stringField(toolCall, 'name') ??
|
|
1008
|
+
stringField(toolCall, 'name') ??
|
|
1009
|
+
stringField(toolCall, 'toolName') ??
|
|
1010
|
+
stringField(toolCall, 'functionName') ??
|
|
1011
|
+
functionToolCallName(toolCall);
|
|
991
1012
|
if (name) {
|
|
992
1013
|
names.push(name);
|
|
993
1014
|
}
|
|
@@ -995,6 +1016,12 @@ function toolCallNames(toolCalls: unknown[] | undefined): string[] {
|
|
|
995
1016
|
return names;
|
|
996
1017
|
}
|
|
997
1018
|
|
|
1019
|
+
/** Read OpenAI-style nested function tool-call names. */
|
|
1020
|
+
function functionToolCallName(toolCall: Record<string, unknown>): string | undefined {
|
|
1021
|
+
const fn = toolCall.function;
|
|
1022
|
+
return isRecord(fn) ? stringField(fn, 'name') : undefined;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
998
1025
|
/** Convert stored message-write usage back into the llm_output usage contract. */
|
|
999
1026
|
function mapHookUsage(usage: unknown): PluginHookLlmOutputEvent['usage'] | undefined {
|
|
1000
1027
|
const mapped = mapUsage(usage);
|
package/src/hooks-backend.ts
CHANGED
|
@@ -376,6 +376,7 @@ export class HookReplayBackend {
|
|
|
376
376
|
private async closeSession(session: SessionState, summary: JsonRecord): Promise<void> {
|
|
377
377
|
drainSession(this.sessionManager(), session);
|
|
378
378
|
closeSessionRoot(this.sessionManager(), session, summary, session.finalOutput ?? summary);
|
|
379
|
+
this.flushSubscriberDelivery('session_close');
|
|
379
380
|
deleteSession(this.stateValue, session);
|
|
380
381
|
}
|
|
381
382
|
|
|
@@ -399,6 +400,18 @@ export class HookReplayBackend {
|
|
|
399
400
|
}
|
|
400
401
|
}
|
|
401
402
|
|
|
403
|
+
/** Wait for native subscriber/exporter delivery after a replay closure boundary. */
|
|
404
|
+
private flushSubscriberDelivery(label: string): void {
|
|
405
|
+
try {
|
|
406
|
+
this.nf.flushSubscribers?.();
|
|
407
|
+
} catch (error) {
|
|
408
|
+
this.logBoundedWarn(
|
|
409
|
+
`flush-subscribers:${label}`,
|
|
410
|
+
`nemo-relay subscriber flush failed: label=${label} error=${toMessage(error)}`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
402
415
|
/** Build the narrow manager interface consumed by focused replay modules. */
|
|
403
416
|
private sessionManager() {
|
|
404
417
|
return {
|
package/src/modules.ts
CHANGED