← Back to Plugins
Voice

Foundry Integration

weijen By weijen 👁 50 views ▲ 0 votes

Azure AI Foundry tracing plugin for OpenClaw — emits OTel GenAI semconv spans (chat / execute_tool) parented to the OpenClaw model.call trace so Foundry's Tracing tab shows conversation id, I/O content, token usage, and tool I/O.

GitHub

Install

npm install --no-save

Configuration Example

{
  "plugins": {
    "allow": ["openclaw-foundry-integration"],
    "entries": {
      "openclaw-foundry-integration": {
        "enabled": true,
        "hooks": {
          // REQUIRED — without this OpenClaw silently denies the
          // llm_input / llm_output / before_tool_call / after_tool_call
          // hooks.
          "allowConversationAccess": true
        },
        "config": {
          "conversationId": {
            "prefix": "oc",
            "hashLength": 16,
            "anonymousId": "oc-anon"
          },
          "captureContent": {
            "inputMessages": true,
            "outputMessages": true,
            "systemPrompt": "hash",       // "off" | "hash" | "full"
            "toolInputs": true,
            "toolOutputs": true,
            "maxAttributeChars": 8000
          }
        }
      }
    },
    "load": {
      "paths": ["~/.openclaw/extensions/openclaw-foundry-integration"]
    }
  }
}

README

# openclaw-foundry-integration

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Single-purpose OpenClaw plugin that turns the gateway's Azure AI Foundry / OpenTelemetry tracing into something useful: every `openclaw.model.call` span carries the **conversation id**, **input messages**, **output messages**, **token usage**, and (optionally) **tool input / output** that Foundry's Tracing tab expects.

---

## Why this plugin exists

OpenClaw 2026.5.x ships two relevant plugins out of the box:

| Plugin | Owns | Limitation |
|--------|------|------------|
| `diagnostics-otel` (bundled) | `openclaw.run` + `openclaw.model.call` spans | Writes provider/model/duration metadata only; **no content, no token counts, no conversation id** |
| `openclaw-conversation-id` (community) | Conversation id | Registers via `api.registerHook(...)` which writes to the **wrong event bus** in the runtime — the hook never fires in production |

Net effect on Foundry: every model trace is missing the three attributes the Tracing tab queries (`gen_ai.conversation.id`, `gen_ai.input.messages`, `gen_ai.output.messages`), so each invocation looks like a disembodied `model.call` with no conversational context.

`openclaw-foundry-integration` fixes both by:

1. Registering on the **typed event bus** via `api.on(...)` (verified by direct probe — see `/memories/repo/openclaw-typed-hooks-api.md`).
2. Looking up the active span via a **`SpanProcessor`** keyed on `(traceId, spanId)`. `trace.getActiveSpan()` returns `null` in every OpenClaw hook because `diagnostics-otel` opens spans imperatively without `setActive`. Each hook context however carries `ctx.trace = {traceId, spanId, ...}` which directly identifies the span we want to write to.
3. Emitting **both** OTel GenAI semconv names (`gen_ai.input.messages`) **and** OpenClaw flat names (`openclaw.content.input_messages`) so existing KQL queries keep working alongside Foundry's standard ones.

---

## What it writes

On every `openclaw.model.call` span:

| Attribute | Source hook | Notes |
|-----------|-------------|-------|
| `gen_ai.conversation.id` | `model_call_started` | sha256-derived from `sessionKey`; never raw chat id |
| `openclaw.conversation.id` | `model_call_started` | same value, flat name |
| `gen_ai.system` | `model_call_started` | provider name |
| `gen_ai.request.model` | `model_call_started` | model id |
| `gen_ai.input.messages` | `llm_input` (replayed) | JSON array `[{role, content}]` of the user prompt, clamped |
| `gen_ai.input.history` | `llm_input` | JSON array of prior messages, clamped |
| `openclaw.content.input_messages` | `llm_input` | raw user prompt text |
| `openclaw.content.system_prompt_sha256` | `llm_input` | always written when systemPrompt mode = `hash` (default) |
| `openclaw.content.system_prompt` | `llm_input` | only when systemPrompt mode = `full` |
| `gen_ai.output.messages` | `llm_output` | JSON array of assistant texts |
| `openclaw.content.output_messages` | `llm_output` | newline-joined assistant texts |
| `gen_ai.usage.input_tokens` / `output_tokens` / `total_tokens` | `llm_output` | from `event.usage` |
| `gen_ai.usage.cache_read_tokens` / `cache_write_tokens` | `llm_output` | when present in usage |
| `openclaw.model.duration_ms` / `ttfb_ms` / `request_bytes` / `response_bytes` / `outcome` | `model_call_ended` | |
| `openclaw.tool.name` / `call_id` | `before_tool_call` | requires `allowConversationAccess: true` |
| `openclaw.content.tool_input` | `before_tool_call` | JSON of arguments |
| `openclaw.content.tool_output` / `tool.success` / `tool.duration_ms` | `after_tool_call` | |

---

## Configuration

```jsonc
{
  "plugins": {
    "allow": ["openclaw-foundry-integration"],
    "entries": {
      "openclaw-foundry-integration": {
        "enabled": true,
        "hooks": {
          // REQUIRED — without this OpenClaw silently denies the
          // llm_input / llm_output / before_tool_call / after_tool_call
          // hooks.
          "allowConversationAccess": true
        },
        "config": {
          "conversationId": {
            "prefix": "oc",
            "hashLength": 16,
            "anonymousId": "oc-anon"
          },
          "captureContent": {
            "inputMessages": true,
            "outputMessages": true,
            "systemPrompt": "hash",       // "off" | "hash" | "full"
            "toolInputs": true,
            "toolOutputs": true,
            "maxAttributeChars": 8000
          }
        }
      }
    },
    "load": {
      "paths": ["~/.openclaw/extensions/openclaw-foundry-integration"]
    }
  }
}
```

`systemPrompt` defaults to `"hash"` because the OpenClaw default agent prompt is ~36 KB of identity text that quickly dominates trace ingestion cost. The hash still lets you correlate "did the prompt change?" without reproducing it on every span.

---

## Installation (Azure VM)

```bash
oc install-foundry-plugin family-claw.multiagentai.co
oc deploy-config         family-claw.multiagentai.co
oc verify-foundry-plugin family-claw.multiagentai.co
oc smoke-test --extended family-claw.multiagentai.co
```

The installer:

1. Downloads / copies the plugin into `~/.openclaw/extensions/openclaw-foundry-integration/`
2. Runs `npm install --no-save --no-package-lock --omit=dev @opentelemetry/api@^1.9.0` inside the plugin dir (the OpenClaw plugin loader walks `node_modules` from the plugin dir, not from the gateway install).
3. Merges the entry into `plugins.allow`, `plugins.entries`, and `plugins.load.paths` with `hooks.allowConversationAccess: true`.
4. Optionally removes the legacy `openclaw-conversation-id` plugin with `--cleanup-old`.

---

## Self-test

```bash
node selftest/synthetic-turn.js
```

Returns exit 0 + JSON `{"ok": true, ...}` when every required attribute lands on a fake span. Exit 1 + JSON listing the missing attributes when the runtime regresses.

---

## Hook payload reference

OpenClaw's typed hook payloads are not officially documented. The shapes this plugin relies on are validated by direct probe (logged at `/memories/repo/openclaw-typed-hooks-api.md` in the upstream operator's repo) and exercised end-to-end by `selftest/synthetic-turn.js` and the unit tests under `test/`.

If a future OpenClaw release changes hook field names, the selftest will fail loudly *before* the rollout reaches production.

---

## License

MIT — see [LICENSE](LICENSE).
voice

Comments

Sign in to leave a comment

Loading comments...