nemo-relay-openclaw 0.3.0-rc.1 → 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
 
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nemo-relay-openclaw",
3
- "version": "0.3.0-rc.1",
3
+ "version": "0.3.0",
4
4
  "description": "NeMo Relay-authored observability plugin for OpenClaw.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "openclaw": "^2026.5.26"
64
64
  },
65
65
  "dependencies": {
66
- "nemo-relay-node": "0.3.0-rc.1"
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);