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 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 does not
16
- rewrite OpenClaw tool execution, provider routing, policy decisions, or model
17
- requests. For middleware-backed behavior that changes execution, use the
18
- patch-based OpenClaw integration from the NeMo Relay repository.
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.github.io/NeMo-Relay](https://nvidia.github.io/NeMo-Relay/).
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 without
208
- changing OpenClaw execution behavior.
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 change OpenClaw tool execution, provider routing, policy decisions,
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 assistantToolCallNames = toolCallNames(event.assistantToolCalls);
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') ?? stringField(toolCall, 'toolName') ?? stringField(toolCall, 'functionName');
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 {
@@ -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-beta.2",
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.12",
46
- "minGatewayVersion": "2026.5.12"
45
+ "pluginApi": ">=2026.5.26",
46
+ "minGatewayVersion": "2026.5.26"
47
47
  },
48
48
  "build": {
49
- "openclawVersion": "2026.5.12",
50
- "pluginSdkVersion": "2026.5.12"
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": ">=2026.5.12"
63
+ "openclaw": "^2026.5.26"
64
64
  },
65
65
  "dependencies": {
66
- "nemo-relay-node": "0.3.0-beta.2"
66
+ "nemo-relay-node": "0.3.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^24.0.0",
@@ -451,12 +451,14 @@ export function buildReplayLlmResponse(
451
451
  config: NemoRelayHookBackendConfig,
452
452
  ): JsonValue {
453
453
  const usage = mapUsage(event.usage);
454
- const assistantToolCallNames = toolCallNames(event.assistantToolCalls);
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') ?? stringField(toolCall, 'toolName') ?? stringField(toolCall, 'functionName');
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);
@@ -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
@@ -41,6 +41,7 @@ export type NemoRelayRuntimeModule = Omit<Pick<typeof NemoRelayRuntime, NemoRela
41
41
  Agent?: Parameters<typeof NemoRelayRuntime.pushScope>[1];
42
42
  }
43
43
  | undefined;
44
+ flushSubscribers?: typeof NemoRelayRuntime.flushSubscribers;
44
45
  };
45
46
 
46
47
  /**