@latitude-data/openclaw-telemetry 0.0.5 → 0.0.6
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 +104 -23
- package/dist/plugin.js +23 -12
- package/dist/plugin.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -12
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -603
- package/dist/cli.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,45 +4,126 @@ OpenClaw plugin that streams every agent run to [Latitude](https://latitude.so)
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
Requires OpenClaw **2026.4.25 or newer
|
|
7
|
+
Requires OpenClaw **2026.4.25 or newer**. Get a Latitude API key + project slug from `https://console.latitude.so/settings/api-keys` first.
|
|
8
|
+
|
|
9
|
+
> **One-shot installer coming soon.** A separate `@latitude-data/openclaw-telemetry-cli` package is in flight that will collapse the steps below into `npx -y @latitude-data/openclaw-telemetry-cli install`. For now, the manual flow below is the supported path.
|
|
10
|
+
|
|
11
|
+
### Step 1 — Install the plugin runtime
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @latitude-data/openclaw-telemetry
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
OpenClaw fetches the package from npm, runs its security scan, copies files into `~/.openclaw/extensions/<id>/`, and creates a (disabled) `plugins.entries["@latitude-data/openclaw-telemetry"]` entry in `~/.openclaw/openclaw.json`. The gateway will print:
|
|
18
|
+
|
|
19
|
+
> Plugin installed but disabled. Configure it, then run `openclaw plugins enable @latitude-data/openclaw-telemetry`.
|
|
20
|
+
|
|
21
|
+
That's expected — the next step does the configure-and-enable.
|
|
22
|
+
|
|
23
|
+
### Step 2 — Configure and enable
|
|
24
|
+
|
|
25
|
+
Run these `openclaw config set` commands (use bracket notation so the scoped package name parses correctly). Substitute your real API key and project slug in the first two:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.apiKey' "lat_xxx"
|
|
29
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.project' "my-project-slug"
|
|
30
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.allowConversationAccess' true
|
|
31
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.allowConversationAccess' true
|
|
32
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].enabled' true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Why two `allowConversationAccess` writes? They mean different things — `hooks.*` is OpenClaw's dispatch gate (without it the plugin's hook handlers never fire), `config.*` is the plugin's payload-content gate (without it the plugin emits structural-only spans). For full content capture they must both be `true`. See [The two flags](#the-two-flags) for details.
|
|
36
|
+
|
|
37
|
+
### Step 3 — Add to `plugins.allow` (optional but loud)
|
|
38
|
+
|
|
39
|
+
OpenClaw warns at every gateway restart about non-bundled plugins that auto-load without provenance via `plugins.allow`. Silence the warning by adding the id:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# `config set` can't append to arrays — set the whole list. If you already have
|
|
43
|
+
# entries in `plugins.allow`, include them here too:
|
|
44
|
+
openclaw config set 'plugins.allow' '["@latitude-data/openclaw-telemetry"]'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Step 4 — Restart the gateway
|
|
8
48
|
|
|
9
49
|
```bash
|
|
10
|
-
npx -y @latitude-data/openclaw-telemetry install
|
|
11
50
|
openclaw gateway restart
|
|
12
51
|
```
|
|
13
52
|
|
|
14
|
-
|
|
53
|
+
Verify everything's wired:
|
|
15
54
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **`config.*`** — credentials, baseUrl, and `allowConversationAccess` (the payload-content gate the plugin's runtime reads).
|
|
20
|
-
- **`hooks.allowConversationAccess`** — the dispatch gate OpenClaw's runtime checks before forwarding LLM/tool/agent events to non-bundled plugins. Always mirrored to the same value as `config.allowConversationAccess`.
|
|
21
|
-
4. Adds the plugin id to `plugins.allow` (running `npx install` is the trust signal). Pass `--no-trust` to opt out.
|
|
55
|
+
```bash
|
|
56
|
+
openclaw config validate --json
|
|
57
|
+
# → {"valid": true, ...}
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
grep -E "blocked|plugin not found|latitude" /tmp/openclaw/openclaw-*.log | tail
|
|
60
|
+
# → ready (N plugins: ..., @latitude-data/openclaw-telemetry, ...)
|
|
61
|
+
# → no "blocked", no "plugin not found"
|
|
62
|
+
```
|
|
24
63
|
|
|
25
|
-
|
|
64
|
+
Send a message to one of your OpenClaw agents — within a few seconds, traces appear at `https://console.latitude.so/projects/<your-slug>`.
|
|
26
65
|
|
|
27
|
-
|
|
28
|
-
| --- | --- |
|
|
29
|
-
| `--api-key=<key>` | Pass the API key instead of being prompted. |
|
|
30
|
-
| `--project=<slug>` | Pass the project slug instead of being prompted. |
|
|
31
|
-
| `--staging` | Target `https://staging.latitude.so` / `https://staging-ingest.latitude.so`. |
|
|
32
|
-
| `--dev` | Target `http://localhost:3000` / `http://localhost:3002`. |
|
|
33
|
-
| `--yes` / `--no-prompt` | Skip all prompts. Required for non-TTY / CI invocations. |
|
|
34
|
-
| `--no-content` | Skip raw prompt/response/tool I/O capture. Spans still emit with timing, token usage, model name, and ids. Mirrored into both `config.allowConversationAccess` and `hooks.allowConversationAccess`. |
|
|
35
|
-
| `--no-trust` | Skip auto-adding the plugin id to `plugins.allow`. OpenClaw will keep printing `plugins.allow is empty` warnings until you add it manually. |
|
|
66
|
+
### Alternative: hand-edit `~/.openclaw/openclaw.json`
|
|
36
67
|
|
|
37
|
-
|
|
68
|
+
If you'd rather paste a JSON block than run six commands, the equivalent edit is:
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
{
|
|
72
|
+
"plugins": {
|
|
73
|
+
"allow": ["@latitude-data/openclaw-telemetry"],
|
|
74
|
+
"entries": {
|
|
75
|
+
"@latitude-data/openclaw-telemetry": {
|
|
76
|
+
"enabled": true,
|
|
77
|
+
"hooks": {
|
|
78
|
+
"allowConversationAccess": true
|
|
79
|
+
},
|
|
80
|
+
"config": {
|
|
81
|
+
"apiKey": "lat_xxx",
|
|
82
|
+
"project": "my-project-slug",
|
|
83
|
+
"allowConversationAccess": true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Merge this with whatever else is in `openclaw.json`. Then `openclaw config validate` and `openclaw gateway restart` as above.
|
|
92
|
+
|
|
93
|
+
### Targeting a different environment
|
|
94
|
+
|
|
95
|
+
For staging or local-dev, also set `baseUrl`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Staging:
|
|
99
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' "https://staging-ingest.latitude.so"
|
|
100
|
+
|
|
101
|
+
# Local dev:
|
|
102
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.baseUrl' "http://localhost:3002"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Structural-only telemetry (no content capture)
|
|
106
|
+
|
|
107
|
+
If you want trace metadata (timings, token usage, model name, agent name, ids) without the prompt/response content, keep `hooks.allowConversationAccess` at `true` (so OpenClaw still dispatches events to us) and set only `config.allowConversationAccess` to `false`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].config.allowConversationAccess' false
|
|
111
|
+
openclaw config set 'plugins.entries["@latitude-data/openclaw-telemetry"].hooks.allowConversationAccess' true
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Setting `hooks.allowConversationAccess=false` would block dispatch entirely — see [The two flags](#the-two-flags). The plugin still emits the full span tree; just the content attributes (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, tool args/results) are scrubbed. Each span carries a `latitude.captured.content: false` boolean so the gate state is visible in the Latitude UI.
|
|
38
115
|
|
|
39
116
|
## Uninstall
|
|
40
117
|
|
|
41
118
|
```bash
|
|
42
|
-
|
|
119
|
+
openclaw plugins uninstall @latitude-data/openclaw-telemetry --force
|
|
43
120
|
```
|
|
44
121
|
|
|
45
|
-
|
|
122
|
+
OpenClaw removes the files, install record, plugin entry, and the `plugins.allow` entry. After that, restart the gateway:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
openclaw gateway restart
|
|
126
|
+
```
|
|
46
127
|
|
|
47
128
|
## What gets sent
|
|
48
129
|
|
package/dist/plugin.js
CHANGED
|
@@ -35,21 +35,32 @@ async function postTraces({ baseUrl, apiKey, project, payload, logger, timeoutMs
|
|
|
35
35
|
//#region src/config.ts
|
|
36
36
|
const DEFAULT_BASE_URL = "https://ingest.latitude.so";
|
|
37
37
|
/**
|
|
38
|
-
* Build a `Config` from OpenClaw's per-plugin config bucket
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK
|
|
39
|
+
* passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)
|
|
40
|
+
* to the registration function — that's the only source.
|
|
41
|
+
*
|
|
42
|
+
* Earlier 0.0.x versions also fell back to environment variables when keys
|
|
43
|
+
* were missing from pluginConfig. That fallback is gone deliberately:
|
|
44
|
+
* OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis
|
|
45
|
+
* security scan that flags any runtime source combining environment-variable
|
|
46
|
+
* access with a network-send call (we have `fetch(` in postTraces). With
|
|
47
|
+
* the fallback our bundled runtime tripped the scanner. The installer
|
|
48
|
+
* writes credentials to `plugins.entries[id].config` anyway, so the
|
|
49
|
+
* fallback was polish-not-feature — its removal also gives a cleaner
|
|
50
|
+
* privacy story (the runtime can't pick up credentials the operator
|
|
51
|
+
* didn't put in openclaw.json).
|
|
52
|
+
*
|
|
53
|
+
* For dev-time testing with debug logs, set `config.debug = true` in
|
|
54
|
+
* openclaw.json directly.
|
|
44
55
|
*/
|
|
45
|
-
function loadConfig(pluginConfig = void 0
|
|
56
|
+
function loadConfig(pluginConfig = void 0) {
|
|
46
57
|
const fromOpts = pluginConfig ?? {};
|
|
47
|
-
const apiKey = pickString(fromOpts.apiKey) ??
|
|
48
|
-
const project = pickString(fromOpts.project) ??
|
|
49
|
-
const baseUrl = pickString(fromOpts.baseUrl) ??
|
|
50
|
-
const debug = pickBool(fromOpts.debug) ??
|
|
58
|
+
const apiKey = pickString(fromOpts.apiKey) ?? "";
|
|
59
|
+
const project = pickString(fromOpts.project) ?? "";
|
|
60
|
+
const baseUrl = pickString(fromOpts.baseUrl) ?? DEFAULT_BASE_URL;
|
|
61
|
+
const debug = pickBool(fromOpts.debug) ?? false;
|
|
51
62
|
const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false;
|
|
52
|
-
const explicitlyDisabled = pickBool(fromOpts.enabled) === false
|
|
63
|
+
const explicitlyDisabled = pickBool(fromOpts.enabled) === false;
|
|
53
64
|
return {
|
|
54
65
|
apiKey,
|
|
55
66
|
baseUrl,
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["safeJson"],"sources":["../src/client.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/messages.ts","../src/span-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","export interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket plus environment\n * variables. The plugin SDK passes `api.pluginConfig` (the user's\n * `plugins.entries[id].config` block) to the registration function — that's\n * the primary source. Env vars are kept as a fallback so existing deployments\n * with `LATITUDE_*` already exported in the gateway environment keep working,\n * and so that `LATITUDE_DEBUG=1` can be flipped without editing openclaw.json.\n */\nexport function loadConfig(\n pluginConfig: Record<string, unknown> | undefined = undefined,\n env: NodeJS.ProcessEnv = process.env,\n): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? env.LATITUDE_API_KEY ?? \"\"\n const project = pickString(fromOpts.project) ?? env.LATITUDE_PROJECT ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? env.LATITUDE_BASE_URL ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? env.LATITUDE_DEBUG === \"1\"\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n // Allow either env var or pluginConfig to disable. Falsy `enabled: false` in\n // pluginConfig wins; otherwise we require both api key and project.\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false || (env.LATITUDE_OPENCLAW_ENABLED ?? \"1\") === \"0\"\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { readFileSync } from \"node:fs\"\nimport { arch, hostname, platform, release } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport type { AttrValue, BuildResult, SpanRecord } from \"./span-builder.ts\"\nimport type { OtlpExportRequest, OtlpKeyValue, OtlpResourceSpans, OtlpSpan } from \"./types.ts\"\n\nconst SCOPE_NAME = \"@latitude-data/openclaw-telemetry\"\nconst SCOPE_VERSION = readScopeVersion()\n\ninterface BuildOptions {\n /**\n * When false, attributes whose key ends in `:gated` are scrubbed from\n * spans before export — that's `gen_ai.input.messages`,\n * `gen_ai.output.messages`, `gen_ai.system_instructions`, `user_prompt`,\n * `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`,\n * `before_compaction.messages`, `before_agent_start.{prompt,messages}`,\n * `agent_end.messages`, and `openclaw.error.message` (the last because\n * error strings can leak prompt/response content). Timing, token usage,\n * model name, ids, agent name, durations, byte counts, and the\n * `latitude.captured.content` boolean are always emitted.\n */\n allowConversationAccess: boolean\n}\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(result: BuildResult, options: BuildOptions): OtlpExportRequest {\n const spans = result.spans.map((span) => toOtlpSpan(span, options))\n const rs: OtlpResourceSpans = {\n resource: { attributes: resourceAttrs() },\n scopeSpans: [{ scope: { name: SCOPE_NAME, version: SCOPE_VERSION }, spans }],\n }\n return { resourceSpans: [rs] }\n}\n\n// ─── SpanRecord → OtlpSpan ─────────────────────────────────────────────────\n\nfunction toOtlpSpan(span: SpanRecord, options: BuildOptions): OtlpSpan {\n const startNs = msToNs(span.startMs)\n const endNs = msToNs(span.endMs ?? span.startMs)\n\n const attrs: OtlpKeyValue[] = []\n for (const [rawKey, value] of Object.entries(span.attrs)) {\n if (value === undefined || value === null) continue\n const isGated = rawKey.endsWith(\":gated\")\n if (isGated && !options.allowConversationAccess) continue\n const key = isGated ? rawKey.slice(0, -\":gated\".length) : rawKey\n const kv = encodeAttr(key, value)\n if (kv !== undefined) attrs.push(kv)\n }\n\n // Always emit the gate state so operators can see it in the UI without\n // needing to grep the original config.\n attrs.push(bool(\"latitude.captured.content\", options.allowConversationAccess))\n // Mirror duration into the canonical name as well, when present.\n if (span.endMs !== undefined) {\n attrs.push(int(\"openclaw.duration_ms.computed\", Math.max(0, span.endMs - span.startMs)))\n }\n\n const statusCode = span.outcome === \"error\" ? 2 : 1\n return {\n traceId: span.traceId,\n spanId: span.spanId,\n parentSpanId: span.parentSpanId,\n name: span.name,\n // OTel SpanKind: 1 = INTERNAL. None of agent/model_call/tool_call/\n // compaction/subagent map cleanly to CLIENT/SERVER/PRODUCER/CONSUMER —\n // OpenClaw is the source-of-truth runtime for all of them.\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: attrs,\n status: { code: statusCode },\n }\n}\n\nfunction encodeAttr(key: string, value: AttrValue): OtlpKeyValue | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value === \"string\") return str(key, value)\n if (typeof value === \"boolean\") return bool(key, value)\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? int(key, value) : { key, value: { doubleValue: value } }\n }\n // Arrays + objects → JSON string. The Latitude UI parses the gen_ai.* keys\n // as JSON; anything else lands as opaque string and is queryable as a\n // contains-substring filter.\n return str(key, safeJson(value))\n}\n\n// ─── Resource + helper attribute encoders ──────────────────────────────────\n\nfunction resourceAttrs(): OtlpKeyValue[] {\n return [\n str(\"service.name\", \"openclaw\"),\n str(\"service.version\", SCOPE_VERSION),\n str(\"host.name\", hostname()),\n str(\"host.arch\", arch()),\n str(\"os.type\", platform()),\n str(\"os.version\", release()),\n ]\n}\n\nfunction str(key: string, value: string): OtlpKeyValue {\n return { key, value: { stringValue: value } }\n}\n\nfunction int(key: string, value: number): OtlpKeyValue {\n return { key, value: { intValue: String(Math.trunc(value)) } }\n}\n\nfunction bool(key: string, value: boolean): OtlpKeyValue {\n return { key, value: { boolValue: value } }\n}\n\nfunction msToNs(ms: number): string {\n return (BigInt(Math.trunc(ms)) * 1_000_000n).toString()\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n\n/**\n * Runtime-read package version, so OTLP `scope.version` and `service.version`\n * always reflect what's actually installed. Read once at module load and\n * cached. Falls back to `\"unknown\"` if the read fails.\n *\n * Same import.meta.url + ../package.json pattern used by `cli.ts` for the\n * `--version` flag — single source of truth in package.json.\n */\nfunction readScopeVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n","/**\n * Normalizes provider-specific message shapes (Anthropic, OpenAI, pi-ai) into\n * the parts-based GenAI format Latitude's parser expects:\n *\n * { role: \"system\" | \"user\" | \"assistant\" | \"tool\", parts: MessagePart[] }\n *\n * Downstream consumers cast `gen_ai.input.messages` and `gen_ai.output.messages`\n * to `GenAIMessage[]` and read `message.parts` directly (e.g. for search\n * indexing). Without normalisation those casts produce objects without\n * `parts`, breaking rendering.\n *\n * The shapes we need to handle:\n *\n * - Anthropic: `{role, content: string}` or `{role, content: ContentBlock[]}`\n * with blocks `{type: \"text\", text}`, `{type: \"tool_use\", id, name, input}`,\n * `{type: \"tool_result\", tool_use_id, content}`, `{type: \"image\", source}`,\n * `{type: \"thinking\", thinking}`.\n * - OpenAI: `{role, content: string}` or with `tool_calls` field.\n * - pi-ai (OpenClaw's wrapper) — superset of the above.\n * - Already-normalized parts-shape messages — passed through unchanged.\n *\n * Anything we don't recognize falls through to a JSON-stringified text part\n * so nothing is silently dropped.\n */\n\nexport interface MessagePart {\n type: string\n content?: string\n // Tool call (assistant invokes a tool)\n id?: string\n name?: string\n arguments?: unknown\n // Tool response (tool replies)\n response?: unknown\n // Image / multimodal\n modality?: string\n uri?: string\n [key: string]: unknown\n}\n\nexport type MessageRole = \"system\" | \"user\" | \"assistant\" | \"tool\"\n\nexport interface Message {\n role: MessageRole\n parts: MessagePart[]\n}\n\nconst ALLOWED_ROLES: ReadonlySet<MessageRole> = new Set([\"system\", \"user\", \"assistant\", \"tool\"])\n\n/**\n * Normalize a single message of any of the provider shapes we know about.\n * Returns `undefined` for non-objects so the caller can skip them.\n */\nexport function normalizeMessage(raw: unknown): Message | undefined {\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const role = coerceRole(obj.role)\n\n // Pre-normalized: already has a parts array.\n if (Array.isArray(obj.parts)) {\n const parts: MessagePart[] = []\n for (const p of obj.parts) {\n if (p && typeof p === \"object\") parts.push(p as MessagePart)\n }\n return { role, parts: parts.length > 0 ? parts : [{ type: \"text\", content: safeJson(raw) }] }\n }\n\n const content = obj.content ?? obj.text ?? obj.message\n\n // OpenAI tool message: `{role: \"tool\", tool_call_id, content}` — handle\n // before the generic string-content branch so we emit a tool_call_response\n // part rather than a plain text part.\n if (role === \"tool\" && obj.tool_call_id !== undefined) {\n return {\n role,\n parts: [\n {\n type: \"tool_call_response\",\n id: typeof obj.tool_call_id === \"string\" ? obj.tool_call_id : \"\",\n response: content ?? safeJson(obj),\n },\n ],\n }\n }\n\n if (typeof content === \"string\") {\n const parts: MessagePart[] = [{ type: \"text\", content }]\n // OpenAI assistant messages may have tool_calls alongside string content.\n appendToolCalls(parts, obj.tool_calls)\n return { role, parts }\n }\n\n if (Array.isArray(content)) {\n const parts: MessagePart[] = []\n for (const block of content) {\n const part = normalizeBlock(block)\n if (part) parts.push(part)\n }\n appendToolCalls(parts, obj.tool_calls)\n if (parts.length === 0) parts.push({ type: \"text\", content: safeJson(content) })\n return { role, parts }\n }\n\n // Unknown shape — dump as JSON so nothing is silently dropped.\n return { role, parts: [{ type: \"text\", content: safeJson(raw) }] }\n}\n\n/** Normalize an array of provider messages. */\nexport function normalizeMessages(raw: unknown[]): Message[] {\n const out: Message[] = []\n for (const m of raw) {\n const norm = normalizeMessage(m)\n if (norm) out.push(norm)\n }\n return out\n}\n\n/** Build a single user message from a string prompt. */\nexport function userMessageFromPrompt(prompt: string): Message {\n return { role: \"user\", parts: [{ type: \"text\", content: prompt }] }\n}\n\n/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */\nexport function assistantMessageFromOutput(assistantTexts: string[], lastAssistant: unknown): Message {\n if (lastAssistant !== undefined) {\n const norm = normalizeMessage(lastAssistant)\n if (norm) return { ...norm, role: \"assistant\" }\n }\n const parts: MessagePart[] = []\n for (const text of assistantTexts) {\n if (text.length > 0) parts.push({ type: \"text\", content: text })\n }\n if (parts.length === 0) parts.push({ type: \"text\", content: \"\" })\n return { role: \"assistant\", parts }\n}\n\n/**\n * Wrap a system prompt string into the parts-array shape expected for\n * `gen_ai.system_instructions`. Empty string in → single empty text part out\n * (still a valid array, never `undefined`).\n */\nexport function systemInstructionsParts(prompt: string): MessagePart[] {\n return [{ type: \"text\", content: prompt }]\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction coerceRole(raw: unknown): MessageRole {\n if (typeof raw !== \"string\") return \"user\"\n return ALLOWED_ROLES.has(raw as MessageRole) ? (raw as MessageRole) : \"user\"\n}\n\nfunction normalizeBlock(raw: unknown): MessagePart | undefined {\n if (typeof raw === \"string\") return { type: \"text\", content: raw }\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n\n // Already a part.\n if (typeof obj.type === \"string\" && (typeof obj.content === \"string\" || obj.content === undefined)) {\n // If it carries our recognized shape (text / tool_call / tool_call_response /\n // uri), pass through. Otherwise fall through to type-specific normalization.\n if (obj.type === \"text\" && typeof obj.content === \"string\") {\n return { type: \"text\", content: obj.content }\n }\n }\n\n const type = typeof obj.type === \"string\" ? obj.type : \"text\"\n\n if (type === \"text\" && typeof obj.text === \"string\") {\n return { type: \"text\", content: obj.text }\n }\n if (type === \"tool_use\") {\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.input ?? {},\n }\n }\n if (type === \"tool_call\") {\n // Already-normalized tool_call part — pass through.\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.arguments ?? obj.input ?? {},\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.tool_use_id === \"string\" ? obj.tool_use_id : \"\",\n response: obj.content ?? \"\",\n }\n }\n if (type === \"tool_call_response\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n response: obj.response ?? \"\",\n }\n }\n if (type === \"thinking\" && typeof obj.thinking === \"string\") {\n return { type: \"reasoning\", content: obj.thinking }\n }\n if (type === \"reasoning\" && typeof obj.content === \"string\") {\n return { type: \"reasoning\", content: obj.content }\n }\n if (type === \"image\" && obj.source && typeof obj.source === \"object\") {\n const src = obj.source as { media_type?: string; data?: string; url?: string }\n const uri = src.url ?? (src.data ? `data:${src.media_type ?? \"image/unknown\"};base64,${src.data}` : \"\")\n if (uri) return { type: \"uri\", modality: \"image\", uri }\n }\n\n // Unknown block type — stringify so nothing is silently dropped.\n return { type, content: safeJson(raw) }\n}\n\n/**\n * OpenAI assistant messages put tool calls in a separate `tool_calls` array\n * alongside string content. Append them as parts so the trace shows what the\n * model emitted in that turn.\n */\nfunction appendToolCalls(parts: MessagePart[], raw: unknown): void {\n if (!Array.isArray(raw)) return\n for (const tc of raw) {\n if (!tc || typeof tc !== \"object\") continue\n const t = tc as Record<string, unknown>\n const fn = t.function as { name?: string; arguments?: string | Record<string, unknown> } | undefined\n let parsedArgs: unknown = fn?.arguments\n if (typeof parsedArgs === \"string\") {\n try {\n parsedArgs = JSON.parse(parsedArgs)\n } catch {\n // leave as string\n }\n }\n parts.push({\n type: \"tool_call\",\n id: typeof t.id === \"string\" ? t.id : \"\",\n name: fn?.name ?? \"\",\n arguments: parsedArgs ?? {},\n })\n }\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","import { createHash, randomUUID } from \"node:crypto\"\nimport {\n assistantMessageFromOutput,\n type Message,\n normalizeMessages,\n systemInstructionsParts,\n userMessageFromPrompt,\n} from \"./messages.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawLlmUsage,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Builds the per-trace span tree for an OpenClaw agent run from the granular\n * paired hooks. Replaces the older `turn-builder.ts` model that collapsed the\n * whole attempt into a single `llm_request` span — that shape was wrong on\n * two counts: `llm_input` / `llm_output` fire ONCE per attempt (not per\n * generation), and an attempt is a sequence of generations interleaved with\n * tool executions.\n *\n * Span set this builder produces:\n *\n * agent (root)\n * ├─ compaction (0..1, rare)\n * ├─ model_call (1..N, one per provider API call)\n * ├─ tool_call: ... (interleaved between model_calls; siblings of agent)\n * ├─ subagent (0..N; child agent runs nest INSIDE these via\n * │ └─ agent ... cross-runId trace propagation)\n * └─ model_call (final)\n *\n * Tool spans are siblings of `agent`, not children of `model_call`, because\n * tools run BETWEEN generations — not during them. Nesting under model_call\n * would falsely imply concurrency.\n *\n * `llm_input` / `llm_output` are NOT span boundaries here. They're data-only\n * feeds that enrich the parent `agent` span (full message history, output\n * messages, aggregate token usage).\n */\n\n// ─── Span record shapes ─────────────────────────────────────────────────────\n\nexport interface SpanRecord {\n /** Stable id for the span (16 hex chars). */\n spanId: string\n /** Span tree id (32 hex chars). */\n traceId: string\n /** Empty string for root agent spans, parent's spanId otherwise. */\n parentSpanId: string\n /** OpenClaw event noun (`agent` / `model_call` / `tool_call` / `compaction` / `subagent`). */\n name: string\n startMs: number\n endMs: number | undefined\n /** Free-form attribute bag — flattened to OTLP key/value at emit time. */\n attrs: Record<string, AttrValue>\n /** Status — set at close from the event payload's outcome/error. */\n outcome?: \"ok\" | \"error\"\n errorMessage?: string | undefined\n}\n\nexport type AttrValue = string | number | boolean | unknown[] | Record<string, unknown> | undefined\n\n// One entry per simple `prefix.field` attribute the builder records on a span.\ntype AttrInput = Record<string, AttrValue>\n\n// ─── Per-run state ──────────────────────────────────────────────────────────\n\ninterface RunState {\n /** Trace root span (the `agent`). */\n agent: SpanRecord\n /**\n * Working snapshot of conversation history in the parts-based GenAI shape\n * Latitude's parser expects. Provider-specific shapes from `llm_input` get\n * normalized once on entry; tool_call / tool_call_response parts appended\n * during the run are already in the right shape.\n */\n history: Message[]\n /** Open per-call spans, keyed on the OpenClaw `callId` from `model_call_started`. */\n openModelCalls: Map<string, SpanRecord>\n /** Open tool spans, keyed on `toolCallId`. */\n openToolCalls: Map<string, SpanRecord>\n /** Open compaction span (at most one in flight). */\n openCompaction: SpanRecord | undefined\n /** All closed spans for this run, ready to emit on agent_end. */\n closed: SpanRecord[]\n /** Any subagent spans we OPENED inside this run, keyed by child runId so we\n * can close them when the child's `subagent_ended` arrives. */\n childSubagentSpans: Map<string, SpanRecord>\n}\n\n/**\n * When a parent's `subagent_spawned` fires we register a link from the\n * child's runId → the parent's traceId + the subagent span's id. Then when the\n * child's `before_agent_start` fires, we use those values so the child's\n * entire span subtree lands inside the parent's trace. Outlives the parent's\n * RunState because the child's `agent_end` may arrive after the parent's.\n *\n * `createdAt` is used by the `evictStaleSubagentLinks` sweep to drop entries\n * whose child runs never reached `agent_end` (gateway crash mid-spawn,\n * plugin reload, etc.). Without the sweep the map grows unbounded over the\n * lifetime of a long-running OpenClaw process.\n */\ninterface SubagentLink {\n traceId: string\n /** Span id of the parent's `subagent` span — used as parentSpanId for the child's root. */\n subagentSpanId: string\n createdAt: number\n}\n\nconst SUBAGENT_LINK_TTL_MS = 60 * 60 * 1000 // 1 hour\nconst SUBAGENT_LINK_MAX = 1000\n\nexport interface BuildResult {\n /** Run id this batch belongs to. */\n runId: string\n /** All spans ready to be exported (agent + everything beneath it). */\n spans: SpanRecord[]\n}\n\n// ─── Builder ────────────────────────────────────────────────────────────────\n\nexport class SpanBuilder {\n private readonly runs = new Map<string, RunState>()\n private readonly subagentLinks = new Map<string, SubagentLink>()\n\n inflightCount(): number {\n return this.runs.size\n }\n\n /**\n * Open the root `agent` span. If the runId was previously registered as a\n * subagent's child, propagate the parent's traceId and parent the new span\n * under the parent's `subagent` span — so the entire subagent's work nests\n * inside the parent's trace as one waterfall.\n */\n onBeforeAgentStart(evt: OpenClawBeforeAgentStartEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n if (this.runs.has(runId)) return // defensive — already open\n\n const link = this.subagentLinks.get(runId)\n const traceId = link?.traceId ?? hashHex(runId, 32)\n const parentSpanId = link?.subagentSpanId ?? \"\"\n\n const agent: SpanRecord = {\n spanId: hashHex(`${traceId}:${runId}:agent`, 16),\n traceId,\n parentSpanId,\n name: \"agent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...flattenCtx(ctx),\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": runId,\n // before_agent_start payload — gated content fields go via `gated.*`\n // attribute keys so the OTLP layer can scrub them without a parallel\n // boolean check. Messages get normalized to parts-shape; `prompt` is\n // a plain string (for the user-prompt convenience attribute).\n \"before_agent_start.prompt:gated\": evt.prompt,\n \"before_agent_start.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n\n this.runs.set(runId, {\n agent,\n history: [],\n openModelCalls: new Map(),\n openToolCalls: new Map(),\n openCompaction: undefined,\n closed: [],\n childSubagentSpans: new Map(),\n })\n }\n\n /**\n * Enrich the open `agent` span with content + identity from the LLM input.\n * Also seeds the rolling history snapshot used by per-call `model_call`\n * input attributes.\n *\n * Provider-specific message shapes get normalized into the parts-based\n * GenAI format here — that's the contract Latitude's downstream parser\n * expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.\n */\n onLlmInput(evt: OpenClawLlmInputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n\n const normalizedHistory = normalizeMessages(evt.historyMessages)\n const inputMessages: Message[] = [...normalizedHistory]\n if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt))\n\n Object.assign(run.agent.attrs, {\n \"gen_ai.system_instructions:gated\": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : undefined,\n \"user_prompt:gated\": evt.prompt,\n \"gen_ai.input.messages:gated\": inputMessages,\n \"openclaw.images.count\": evt.imagesCount,\n \"gen_ai.request.model\": evt.model,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n })\n // Seed the rolling history with the normalized form. The per-call\n // model_call_started will copy the snapshot at the time it fires;\n // subsequent before_tool_call / after_tool_call events append to the\n // same array (already in parts shape) so the next model_call captures\n // the post-tool state.\n run.history = inputMessages\n }\n\n /**\n * Enrich the agent span with attempt-aggregate output + token usage.\n * (Per-call usage isn't surfaced by OpenClaw today — see PR #2986.)\n */\n onLlmOutput(evt: OpenClawLlmOutputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant)\n Object.assign(run.agent.attrs, {\n \"gen_ai.output.messages:gated\": [assistantMessage],\n \"openclaw.resolved.ref\": evt.resolvedRef,\n \"openclaw.harness.id\": evt.harnessId,\n \"gen_ai.response.model\": evt.model,\n ...usageAttrs(evt.usage),\n })\n }\n\n onModelCallStarted(evt: OpenClawModelCallStartedEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"model_call\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"openclaw.call.id\": evt.callId,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n \"gen_ai.request.model\": evt.model,\n \"openclaw.api\": evt.api,\n \"openclaw.transport\": evt.transport,\n // Snapshot the rolling history at the moment this generation starts.\n // The model saw exactly this state. Gated.\n \"gen_ai.input.messages:gated\": [...run.history],\n },\n }\n run.openModelCalls.set(evt.callId, span)\n }\n\n onModelCallEnded(evt: OpenClawModelCallEndedEvent, _ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span = run.openModelCalls.get(evt.callId)\n if (!span) return\n span.endMs = Date.now()\n span.outcome = evt.outcome === \"completed\" ? \"ok\" : \"error\"\n span.errorMessage = evt.errorCategory\n Object.assign(span.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.outcome\": evt.outcome,\n \"openclaw.error.category\": evt.errorCategory,\n \"openclaw.failure.kind\": evt.failureKind,\n \"openclaw.request.payload_bytes\": evt.requestPayloadBytes,\n \"openclaw.response.stream_bytes\": evt.responseStreamBytes,\n \"openclaw.ttfb_ms\": evt.timeToFirstByteMs,\n \"openclaw.upstream.request_id_hash\": evt.upstreamRequestIdHash,\n })\n run.openModelCalls.delete(evt.callId)\n run.closed.push(span)\n }\n\n /**\n * Open a `tool_call` span as a sibling of the agent span. Also append a\n * synthetic assistant `tool_call` part to the rolling history so the NEXT\n * model_call's input snapshot reflects what the model emitted.\n *\n * IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning\n * anything other than `undefined`/falsy from this handler blocks the tool.\n * The plugin-side handler enforces a void return; this method's signature\n * already returns `void`.\n */\n onBeforeToolCall(evt: OpenClawBeforeToolCallEvent, ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: `tool_call:${evt.toolName}`,\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"gen_ai.tool.name\": evt.toolName,\n \"gen_ai.tool.call.id\": toolCallId,\n \"gen_ai.tool.call.arguments:gated\": evt.params,\n },\n }\n run.openToolCalls.set(toolCallId, span)\n\n // Append an assistant tool_call part to the rolling history so the next\n // model_call captures it.\n run.history.push({\n role: \"assistant\",\n parts: [{ type: \"tool_call\", id: toolCallId, name: evt.toolName, arguments: evt.params }],\n })\n }\n\n onAfterToolCall(evt: OpenClawAfterToolCallEvent, _ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n // Match priority:\n // 1. Direct id lookup — the happy path.\n // 2. Name-match fallback — if `evt.toolCallId` is missing OR refers to\n // an id we didn't open (e.g. before_tool_call elided it and we\n // synthesised one, then after_tool_call provided the real one).\n //\n // Without the id-mismatch fallback, the open span would never close\n // and would get force-closed as `abandoned` at agent_end.\n let resolvedId: string | undefined =\n evt.toolCallId && run.openToolCalls.has(evt.toolCallId) ? evt.toolCallId : undefined\n if (!resolvedId) resolvedId = this.findOpenToolCallByName(run, evt.toolName)\n if (!resolvedId) return\n const span = run.openToolCalls.get(resolvedId)\n if (!span) return\n const toolCallId: string = resolvedId\n\n span.endMs = Date.now()\n const isError = Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"gen_ai.tool.call.result:gated\": evt.result,\n \"openclaw.error.message:gated\": evt.error,\n \"openclaw.duration_ms\": evt.durationMs,\n })\n run.openToolCalls.delete(toolCallId)\n run.closed.push(span)\n\n // Append the tool response to the rolling history so the next\n // model_call's input snapshot includes it.\n run.history.push({\n role: \"tool\",\n parts: [{ type: \"tool_call_response\", id: toolCallId, response: evt.result ?? evt.error ?? \"\" }],\n })\n }\n\n onBeforeCompaction(evt: OpenClawBeforeCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:compaction:${run.closed.length}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"compaction\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": runId,\n \"openclaw.compaction.message_count.before\": evt.messageCount,\n \"openclaw.compaction.session_file\": evt.sessionFile,\n \"before_compaction.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n run.openCompaction = span\n }\n\n onAfterCompaction(evt: OpenClawAfterCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run?.openCompaction) return\n const span = run.openCompaction\n span.endMs = Date.now()\n span.outcome = \"ok\"\n Object.assign(span.attrs, {\n \"openclaw.compaction.message_count.after\": evt.messageCount,\n \"openclaw.compaction.compacted_count\": evt.compactedCount,\n \"openclaw.compaction.token_count\": evt.tokenCount,\n })\n run.openCompaction = undefined\n run.closed.push(span)\n }\n\n /**\n * Open a `subagent` span on the parent run AND register the cross-run link\n * so the child's `before_agent_start` can find us.\n */\n onSubagentSpawned(evt: OpenClawSubagentSpawnedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n\n const span: SpanRecord = {\n spanId: hashHex(`${parent.agent.traceId}:subagent:${evt.runId}`, 16),\n traceId: parent.agent.traceId,\n parentSpanId: parent.agent.spanId,\n name: \"subagent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n // The subagent span lives inside the parent's trace — tags +\n // metadata reflect the parent's ctx (which agent/channel/trigger\n // is doing the spawning), not the child's. The child's `agent` span\n // (opened later by the child run's before_agent_start) carries\n // its OWN ctx.\n ...latitudeAttrs(ctx),\n \"openclaw.parent.run.id\": parentRunId,\n \"openclaw.run.id\": evt.runId,\n \"openclaw.subagent.child_session_key\": evt.childSessionKey,\n \"openclaw.subagent.agent_id\": evt.agentId,\n \"openclaw.subagent.label\": evt.label,\n \"openclaw.subagent.mode\": evt.mode,\n \"openclaw.subagent.thread_requested\": evt.threadRequested,\n \"openclaw.subagent.requester.channel\": evt.requester?.channel,\n \"openclaw.subagent.requester.account_id\": evt.requester?.accountId,\n \"openclaw.subagent.requester.to\": evt.requester?.to,\n \"openclaw.subagent.requester.thread_id\":\n typeof evt.requester?.threadId === \"string\" || typeof evt.requester?.threadId === \"number\"\n ? String(evt.requester.threadId)\n : undefined,\n },\n }\n parent.childSubagentSpans.set(evt.runId, span)\n this.evictStaleSubagentLinks()\n this.subagentLinks.set(evt.runId, {\n traceId: parent.agent.traceId,\n subagentSpanId: span.spanId,\n createdAt: Date.now(),\n })\n }\n\n onSubagentEnded(evt: OpenClawSubagentEndedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n const childRunId = evt.runId\n if (!childRunId) return\n const span = parent.childSubagentSpans.get(childRunId)\n if (!span) return\n\n span.endMs = Date.now()\n const isError = evt.outcome === \"error\" || Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"openclaw.subagent.target_session_key\": evt.targetSessionKey,\n \"openclaw.subagent.target_kind\": evt.targetKind,\n \"openclaw.subagent.reason\": evt.reason,\n \"openclaw.subagent.outcome\": evt.outcome,\n \"openclaw.subagent.send_farewell\": evt.sendFarewell,\n \"openclaw.subagent.account_id\": evt.accountId,\n })\n parent.childSubagentSpans.delete(childRunId)\n parent.closed.push(span)\n // Don't delete the subagent link yet — the child's `agent_end` may still\n // be in flight. We clean it up when the child's agent_end fires.\n }\n\n /**\n * Close out the run: finish the agent span, abandon any still-open\n * model_calls / tool_calls / compactions, and return everything ready to\n * emit. Removes the subagent link if this was a child run.\n */\n onAgentEnd(evt: OpenClawAgentEndEvent, ctx: OpenClawAgentContext): BuildResult | undefined {\n const runId = ctx.runId\n if (!runId) return undefined\n const run = this.runs.get(runId)\n if (!run) {\n // Child agent run that closed without ever opening — drop the link\n // entry so the map doesn't grow unbounded.\n this.subagentLinks.delete(runId)\n return undefined\n }\n\n const now = Date.now()\n run.agent.endMs = now\n run.agent.outcome = evt.success ? \"ok\" : \"error\"\n run.agent.errorMessage = evt.error\n Object.assign(run.agent.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.run.success\": evt.success,\n \"openclaw.error.message:gated\": evt.error,\n \"agent_end.messages:gated\": normalizeMessages(evt.messages),\n })\n\n // Anything still open at agent_end didn't get a proper close event.\n // Mark them abandoned and force-close so they show up in the trace\n // rather than vanish silently.\n for (const span of run.openModelCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n for (const span of run.openToolCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n if (run.openCompaction) {\n run.openCompaction.endMs = now\n run.openCompaction.outcome = \"error\"\n run.openCompaction.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(run.openCompaction)\n }\n for (const span of run.childSubagentSpans.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.subagent.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n\n const spans = [run.agent, ...run.closed]\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n return { runId, spans }\n }\n\n /** Drop a run without emitting — used on errors from the emit path. */\n abandon(runId: string): void {\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n }\n\n /** Test-only: how many cross-run subagent links we're holding. */\n subagentLinkCount(): number {\n return this.subagentLinks.size\n }\n\n /**\n * Drop any subagent links whose child run never reached `agent_end`. Called\n * before every `subagent_spawned` insert so the map stays bounded even when\n * children crash mid-spawn or the plugin reloads.\n *\n * Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),\n * then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the\n * oldest until we're under).\n */\n private evictStaleSubagentLinks(): void {\n const now = Date.now()\n for (const [runId, link] of this.subagentLinks) {\n if (now - link.createdAt > SUBAGENT_LINK_TTL_MS) {\n this.subagentLinks.delete(runId)\n }\n }\n if (this.subagentLinks.size <= SUBAGENT_LINK_MAX) return\n const sorted = Array.from(this.subagentLinks.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt)\n const toRemove = this.subagentLinks.size - SUBAGENT_LINK_MAX\n for (let i = 0; i < toRemove; i++) {\n const entry = sorted[i]\n if (entry) this.subagentLinks.delete(entry[0])\n }\n }\n\n private findOpenToolCallByName(run: RunState, toolName: string): string | undefined {\n // Defensive: OpenClaw versions that elide toolCallId on after_tool_call\n // can be matched by name + still-open status. When multiple in-flight\n // tool calls share a name, prefer the MOST RECENTLY opened — Maps\n // preserve insertion order, so iterating in reverse picks the latest.\n // (LIFO matches typical agent runtimes that issue tools sequentially.)\n const target = `tool_call:${toolName}`\n const entries = Array.from(run.openToolCalls.entries())\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i]\n if (!entry) continue\n const [id, span] = entry\n if (span.name === target) return id\n }\n return undefined\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction flattenCtx(ctx: OpenClawAgentContext): AttrInput {\n return {\n \"openclaw.run.id\": ctx.runId,\n \"openclaw.session.id\": ctx.sessionId,\n \"openclaw.session.key\": ctx.sessionKey,\n \"openclaw.agent.id\": ctx.agentId,\n \"openclaw.agent.name\": ctx.agentId,\n \"openclaw.workspace.dir\": ctx.workspaceDir,\n \"openclaw.message.provider\": ctx.messageProvider,\n \"openclaw.trigger\": ctx.trigger,\n \"openclaw.channel.id\": ctx.channelId,\n \"openclaw.cron.job.id\": ctx.jobId,\n \"openclaw.model.provider.id\": ctx.modelProviderId,\n \"openclaw.model.id\": ctx.modelId,\n }\n}\n\n/**\n * Build `latitude.tags` and `latitude.metadata` attrs from the hook context.\n * The OTLP encoder JSON-stringifies arrays/objects, which is the encoding\n * Latitude's resolver expects:\n *\n * - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`\n * in domain/spans/src/otlp/resolvers/enrichment.ts).\n * - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).\n *\n * Tags = the agent id, the channel id, and the trigger. When trigger is\n * `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the\n * specific cron job. Each tag is conditionally included so absent ctx\n * fields don't produce empty entries.\n *\n * Metadata = every ctx field that's set, namespaced under `openclaw.*` so\n * it can't collide with metadata keys other plugins might emit.\n */\nfunction latitudeAttrs(ctx: OpenClawAgentContext): AttrInput {\n const tags: string[] = []\n if (ctx.agentId) tags.push(ctx.agentId)\n if (ctx.channelId) tags.push(ctx.channelId)\n if (ctx.trigger) {\n tags.push(ctx.trigger === \"cron\" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger)\n }\n\n const metadata: Record<string, string> = {}\n if (ctx.runId) metadata[\"openclaw.run.id\"] = ctx.runId\n if (ctx.sessionId) metadata[\"openclaw.session.id\"] = ctx.sessionId\n if (ctx.sessionKey) metadata[\"openclaw.session.key\"] = ctx.sessionKey\n if (ctx.agentId) metadata[\"openclaw.agent.id\"] = ctx.agentId\n if (ctx.workspaceDir) metadata[\"openclaw.workspace.dir\"] = ctx.workspaceDir\n if (ctx.channelId) metadata[\"openclaw.channel.id\"] = ctx.channelId\n if (ctx.messageProvider) metadata[\"openclaw.message.provider\"] = ctx.messageProvider\n if (ctx.trigger) metadata[\"openclaw.trigger\"] = ctx.trigger\n if (ctx.jobId) metadata[\"openclaw.cron.job.id\"] = ctx.jobId\n if (ctx.modelProviderId) metadata[\"openclaw.model.provider.id\"] = ctx.modelProviderId\n if (ctx.modelId) metadata[\"openclaw.model.id\"] = ctx.modelId\n\n return {\n \"latitude.tags\": tags.length > 0 ? tags : undefined,\n \"latitude.metadata\": Object.keys(metadata).length > 0 ? metadata : undefined,\n }\n}\n\nfunction usageAttrs(usage: OpenClawLlmUsage | undefined): AttrInput {\n if (!usage) return {}\n return {\n \"gen_ai.usage.input_tokens\": usage.input,\n \"gen_ai.usage.output_tokens\": usage.output,\n \"gen_ai.usage.cache_read_input_tokens\": usage.cacheRead,\n \"gen_ai.usage.cache_creation_input_tokens\": usage.cacheWrite,\n \"gen_ai.usage.total_tokens\": usage.total,\n }\n}\n\nfunction hashHex(input: string, length: number): string {\n return createHash(\"sha256\").update(input).digest(\"hex\").slice(0, length)\n}\n","import { postTraces } from \"./client.ts\"\nimport { type Config, loadConfig } from \"./config.ts\"\nimport { createLogger, type Logger } from \"./logger.ts\"\nimport { buildOtlpRequest } from \"./otlp.ts\"\nimport { type BuildResult, SpanBuilder } from \"./span-builder.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Minimal structural type for OpenClaw's plugin API — only the fields we\n * touch. We avoid importing from `openclaw/plugin-sdk` so the package stays\n * usable when OpenClaw isn't installed (the CLI and tests don't need it),\n * and so we're robust to small signature changes across OpenClaw versions.\n *\n * `pluginConfig` is the user's `plugins.entries[id].config` block — that's\n * the canonical place to read credentials and feature flags. The OpenClaw\n * plugin SDK also exposes the same value as `api.pluginConfig` on the\n * builder API; keep both names in sync if the upstream contract evolves.\n */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\n pluginConfig?: Record<string, unknown>\n on: <K extends string>(\n hookName: K,\n handler: (event: unknown, ctx: unknown) => unknown,\n opts?: { priority?: number },\n ) => void\n}\n\nexport interface RegisterOptions {\n /** Override the config, mostly for tests. */\n config?: Config\n /** Override the logger. */\n logger?: Logger\n /**\n * Hook to observe the emitted run right before it's posted. Used by tests;\n * not a stable public API.\n */\n onEmit?: (result: BuildResult) => void\n}\n\n/**\n * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls\n * this once at plugin activation; we wire up the granular paired hooks\n * (model_call_started/_ended, before_/after_tool_call, before_/after_compaction,\n * subagent_spawned/_ended, before_agent_start/agent_end) plus the\n * data-only feeds (llm_input/llm_output) that enrich the agent span.\n *\n * Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying\n * hooks; before_tool_call is a `runModifyingHook` where returning anything\n * other than undefined blocks the tool call. Our handler returns nothing —\n * keep it that way.\n */\nexport default function registerLatitudePlugin(api: OpenClawPluginApiLike, opts: RegisterOptions = {}): void {\n // Source of truth: OpenClaw passes the user's `plugins.entries[id].config`\n // as `api.pluginConfig`. Env vars are a fallback so existing deploys with\n // LATITUDE_* exported in the gateway environment keep working.\n const config = opts.config ?? loadConfig(api.pluginConfig)\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: apiKey is empty (set plugins.entries[id].config.apiKey)\")\n if (config.project === \"\") logger.debug(\"disabled: project is empty (set plugins.entries[id].config.project)\")\n return\n }\n logger.debug(\n `enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`,\n )\n\n const builder = new SpanBuilder()\n\n // Helper: wrap a void-returning hook handler with try/catch + cast.\n const wrap = <E>(\n name: string,\n fn: (evt: E, ctx: OpenClawAgentContext) => void,\n ): ((evt: unknown, ctx: unknown) => void) => {\n return (evt, ctx) => {\n try {\n fn(evt as E, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`${name} handler failed: ${String(err)}`)\n }\n }\n }\n\n // ─── Span boundaries ────────────────────────────────────────────────────\n\n api.on(\n \"before_agent_start\",\n wrap<OpenClawBeforeAgentStartEvent>(\"before_agent_start\", (evt, ctx) => {\n builder.onBeforeAgentStart(evt, ctx)\n }),\n )\n\n api.on(\n \"model_call_started\",\n wrap<OpenClawModelCallStartedEvent>(\"model_call_started\", (evt, ctx) => {\n builder.onModelCallStarted(evt, ctx)\n }),\n )\n api.on(\n \"model_call_ended\",\n wrap<OpenClawModelCallEndedEvent>(\"model_call_ended\", (evt, ctx) => {\n builder.onModelCallEnded(evt, ctx)\n }),\n )\n\n // before_tool_call is a `runModifyingHook` — returning {block: true} from\n // any plugin handler blocks the tool. We return nothing (void) so OpenClaw\n // dispatches normally. The `wrap` helper preserves that void return.\n api.on(\n \"before_tool_call\",\n wrap<OpenClawBeforeToolCallEvent>(\"before_tool_call\", (evt, ctx) => {\n builder.onBeforeToolCall(evt, ctx)\n }),\n )\n api.on(\n \"after_tool_call\",\n wrap<OpenClawAfterToolCallEvent>(\"after_tool_call\", (evt, ctx) => {\n builder.onAfterToolCall(evt, ctx)\n }),\n )\n\n api.on(\n \"before_compaction\",\n wrap<OpenClawBeforeCompactionEvent>(\"before_compaction\", (evt, ctx) => {\n builder.onBeforeCompaction(evt, ctx)\n }),\n )\n api.on(\n \"after_compaction\",\n wrap<OpenClawAfterCompactionEvent>(\"after_compaction\", (evt, ctx) => {\n builder.onAfterCompaction(evt, ctx)\n }),\n )\n\n api.on(\n \"subagent_spawned\",\n wrap<OpenClawSubagentSpawnedEvent>(\"subagent_spawned\", (evt, ctx) => {\n builder.onSubagentSpawned(evt, ctx)\n }),\n )\n api.on(\n \"subagent_ended\",\n wrap<OpenClawSubagentEndedEvent>(\"subagent_ended\", (evt, ctx) => {\n builder.onSubagentEnded(evt, ctx)\n }),\n )\n\n // ─── Data-only feeds ────────────────────────────────────────────────────\n // These DON'T open or close spans. They enrich the open `agent` span with\n // attempt-aggregate content + token usage, and seed the rolling history\n // snapshot used by per-call `model_call.input.messages`.\n\n api.on(\n \"llm_input\",\n wrap<OpenClawLlmInputEvent>(\"llm_input\", (evt, ctx) => {\n builder.onLlmInput(evt, ctx)\n }),\n )\n api.on(\n \"llm_output\",\n wrap<OpenClawLlmOutputEvent>(\"llm_output\", (evt, ctx) => {\n builder.onLlmOutput(evt, ctx)\n }),\n )\n\n // ─── Trace flush ────────────────────────────────────────────────────────\n\n api.on(\n \"agent_end\",\n wrap<OpenClawAgentEndEvent>(\"agent_end\", (evt, ctx) => {\n const result = builder.onAgentEnd(evt, ctx)\n if (!result) {\n logger.debug(\"agent_end fired without a matching run in flight\")\n return\n }\n opts.onEmit?.(result)\n const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess })\n void postTraces({\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n project: config.project,\n payload,\n logger,\n })\n }),\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["safeJson"],"sources":["../src/client.ts","../src/config.ts","../src/logger.ts","../src/otlp.ts","../src/messages.ts","../src/span-builder.ts","../src/plugin.ts"],"sourcesContent":["import type { Logger } from \"./logger.ts\"\nimport type { OtlpExportRequest } from \"./types.ts\"\n\nexport async function postTraces({\n baseUrl,\n apiKey,\n project,\n payload,\n logger,\n timeoutMs = 10_000,\n}: {\n baseUrl: string\n apiKey: string\n project: string\n payload: OtlpExportRequest\n logger: Logger\n timeoutMs?: number\n}): Promise<void> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/v1/traces`\n const bodyText = JSON.stringify(payload)\n logger.debug(`POST ${url} (project=${project}, ${bodyText.length} bytes)`)\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n \"X-Latitude-Project\": project,\n },\n body: bodyText,\n signal: controller.signal,\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n logger.warn(`ingest HTTP ${res.status}: ${text.slice(0, 500)}`)\n } else {\n logger.debug(`ingest HTTP ${res.status}`)\n }\n } catch (err) {\n logger.warn(`ingest failed: ${String(err)}`)\n } finally {\n clearTimeout(timer)\n }\n}\n","export interface Config {\n apiKey: string\n baseUrl: string\n project: string\n enabled: boolean\n debug: boolean\n /**\n * When false, the plugin still emits one span per LLM call / tool / run, but\n * scrubs raw conversation content (input/output messages, system prompt,\n * tool args, tool results, the surfaced first-prompt). Token counts, model\n * names, agent ids, and timings are unaffected.\n */\n allowConversationAccess: boolean\n}\n\nconst DEFAULT_BASE_URL = \"https://ingest.latitude.so\"\n\n/**\n * Build a `Config` from OpenClaw's per-plugin config bucket. The plugin SDK\n * passes `api.pluginConfig` (the user's `plugins.entries[id].config` block)\n * to the registration function — that's the only source.\n *\n * Earlier 0.0.x versions also fell back to environment variables when keys\n * were missing from pluginConfig. That fallback is gone deliberately:\n * OpenClaw 2026.4.25's `openclaw plugins install` runs a static-analysis\n * security scan that flags any runtime source combining environment-variable\n * access with a network-send call (we have `fetch(` in postTraces). With\n * the fallback our bundled runtime tripped the scanner. The installer\n * writes credentials to `plugins.entries[id].config` anyway, so the\n * fallback was polish-not-feature — its removal also gives a cleaner\n * privacy story (the runtime can't pick up credentials the operator\n * didn't put in openclaw.json).\n *\n * For dev-time testing with debug logs, set `config.debug = true` in\n * openclaw.json directly.\n */\nexport function loadConfig(pluginConfig: Record<string, unknown> | undefined = undefined): Config {\n const fromOpts = pluginConfig ?? {}\n\n const apiKey = pickString(fromOpts.apiKey) ?? \"\"\n const project = pickString(fromOpts.project) ?? \"\"\n const baseUrl = pickString(fromOpts.baseUrl) ?? DEFAULT_BASE_URL\n\n const debug = pickBool(fromOpts.debug) ?? false\n const allowConversationAccess = pickBool(fromOpts.allowConversationAccess) ?? false\n\n const explicitlyDisabled = pickBool(fromOpts.enabled) === false\n const hasCreds = apiKey !== \"\" && project !== \"\"\n\n return {\n apiKey,\n baseUrl,\n project,\n debug,\n allowConversationAccess,\n enabled: hasCreds && !explicitlyDisabled,\n }\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\nfunction pickBool(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined\n}\n","const PREFIX = \"[latitude-openclaw]\"\n\nexport interface Logger {\n debug: (msg: string) => void\n warn: (msg: string) => void\n}\n\nexport function createLogger(debugEnabled: boolean): Logger {\n return {\n debug: debugEnabled ? (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`) : () => {},\n warn: (msg) => process.stderr.write(`${PREFIX} ${msg}\\n`),\n }\n}\n","import { readFileSync } from \"node:fs\"\nimport { arch, hostname, platform, release } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport type { AttrValue, BuildResult, SpanRecord } from \"./span-builder.ts\"\nimport type { OtlpExportRequest, OtlpKeyValue, OtlpResourceSpans, OtlpSpan } from \"./types.ts\"\n\nconst SCOPE_NAME = \"@latitude-data/openclaw-telemetry\"\nconst SCOPE_VERSION = readScopeVersion()\n\ninterface BuildOptions {\n /**\n * When false, attributes whose key ends in `:gated` are scrubbed from\n * spans before export — that's `gen_ai.input.messages`,\n * `gen_ai.output.messages`, `gen_ai.system_instructions`, `user_prompt`,\n * `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`,\n * `before_compaction.messages`, `before_agent_start.{prompt,messages}`,\n * `agent_end.messages`, and `openclaw.error.message` (the last because\n * error strings can leak prompt/response content). Timing, token usage,\n * model name, ids, agent name, durations, byte counts, and the\n * `latitude.captured.content` boolean are always emitted.\n */\n allowConversationAccess: boolean\n}\n\n/** Build an OTLP export request for a single completed agent run. */\nexport function buildOtlpRequest(result: BuildResult, options: BuildOptions): OtlpExportRequest {\n const spans = result.spans.map((span) => toOtlpSpan(span, options))\n const rs: OtlpResourceSpans = {\n resource: { attributes: resourceAttrs() },\n scopeSpans: [{ scope: { name: SCOPE_NAME, version: SCOPE_VERSION }, spans }],\n }\n return { resourceSpans: [rs] }\n}\n\n// ─── SpanRecord → OtlpSpan ─────────────────────────────────────────────────\n\nfunction toOtlpSpan(span: SpanRecord, options: BuildOptions): OtlpSpan {\n const startNs = msToNs(span.startMs)\n const endNs = msToNs(span.endMs ?? span.startMs)\n\n const attrs: OtlpKeyValue[] = []\n for (const [rawKey, value] of Object.entries(span.attrs)) {\n if (value === undefined || value === null) continue\n const isGated = rawKey.endsWith(\":gated\")\n if (isGated && !options.allowConversationAccess) continue\n const key = isGated ? rawKey.slice(0, -\":gated\".length) : rawKey\n const kv = encodeAttr(key, value)\n if (kv !== undefined) attrs.push(kv)\n }\n\n // Always emit the gate state so operators can see it in the UI without\n // needing to grep the original config.\n attrs.push(bool(\"latitude.captured.content\", options.allowConversationAccess))\n // Mirror duration into the canonical name as well, when present.\n if (span.endMs !== undefined) {\n attrs.push(int(\"openclaw.duration_ms.computed\", Math.max(0, span.endMs - span.startMs)))\n }\n\n const statusCode = span.outcome === \"error\" ? 2 : 1\n return {\n traceId: span.traceId,\n spanId: span.spanId,\n parentSpanId: span.parentSpanId,\n name: span.name,\n // OTel SpanKind: 1 = INTERNAL. None of agent/model_call/tool_call/\n // compaction/subagent map cleanly to CLIENT/SERVER/PRODUCER/CONSUMER —\n // OpenClaw is the source-of-truth runtime for all of them.\n kind: 1,\n startTimeUnixNano: startNs,\n endTimeUnixNano: endNs,\n attributes: attrs,\n status: { code: statusCode },\n }\n}\n\nfunction encodeAttr(key: string, value: AttrValue): OtlpKeyValue | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value === \"string\") return str(key, value)\n if (typeof value === \"boolean\") return bool(key, value)\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? int(key, value) : { key, value: { doubleValue: value } }\n }\n // Arrays + objects → JSON string. The Latitude UI parses the gen_ai.* keys\n // as JSON; anything else lands as opaque string and is queryable as a\n // contains-substring filter.\n return str(key, safeJson(value))\n}\n\n// ─── Resource + helper attribute encoders ──────────────────────────────────\n\nfunction resourceAttrs(): OtlpKeyValue[] {\n return [\n str(\"service.name\", \"openclaw\"),\n str(\"service.version\", SCOPE_VERSION),\n str(\"host.name\", hostname()),\n str(\"host.arch\", arch()),\n str(\"os.type\", platform()),\n str(\"os.version\", release()),\n ]\n}\n\nfunction str(key: string, value: string): OtlpKeyValue {\n return { key, value: { stringValue: value } }\n}\n\nfunction int(key: string, value: number): OtlpKeyValue {\n return { key, value: { intValue: String(Math.trunc(value)) } }\n}\n\nfunction bool(key: string, value: boolean): OtlpKeyValue {\n return { key, value: { boolValue: value } }\n}\n\nfunction msToNs(ms: number): string {\n return (BigInt(Math.trunc(ms)) * 1_000_000n).toString()\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n\n/**\n * Runtime-read package version, so OTLP `scope.version` and `service.version`\n * always reflect what's actually installed. Read once at module load and\n * cached. Falls back to `\"unknown\"` if the read fails.\n *\n * Same import.meta.url + ../package.json pattern used by `cli.ts` for the\n * `--version` flag — single source of truth in package.json.\n */\nfunction readScopeVersion(): string {\n try {\n const here = dirname(fileURLToPath(import.meta.url))\n const pkgPath = join(here, \"..\", \"package.json\")\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as { version?: string }\n return pkg.version ?? \"unknown\"\n } catch {\n return \"unknown\"\n }\n}\n","/**\n * Normalizes provider-specific message shapes (Anthropic, OpenAI, pi-ai) into\n * the parts-based GenAI format Latitude's parser expects:\n *\n * { role: \"system\" | \"user\" | \"assistant\" | \"tool\", parts: MessagePart[] }\n *\n * Downstream consumers cast `gen_ai.input.messages` and `gen_ai.output.messages`\n * to `GenAIMessage[]` and read `message.parts` directly (e.g. for search\n * indexing). Without normalisation those casts produce objects without\n * `parts`, breaking rendering.\n *\n * The shapes we need to handle:\n *\n * - Anthropic: `{role, content: string}` or `{role, content: ContentBlock[]}`\n * with blocks `{type: \"text\", text}`, `{type: \"tool_use\", id, name, input}`,\n * `{type: \"tool_result\", tool_use_id, content}`, `{type: \"image\", source}`,\n * `{type: \"thinking\", thinking}`.\n * - OpenAI: `{role, content: string}` or with `tool_calls` field.\n * - pi-ai (OpenClaw's wrapper) — superset of the above.\n * - Already-normalized parts-shape messages — passed through unchanged.\n *\n * Anything we don't recognize falls through to a JSON-stringified text part\n * so nothing is silently dropped.\n */\n\nexport interface MessagePart {\n type: string\n content?: string\n // Tool call (assistant invokes a tool)\n id?: string\n name?: string\n arguments?: unknown\n // Tool response (tool replies)\n response?: unknown\n // Image / multimodal\n modality?: string\n uri?: string\n [key: string]: unknown\n}\n\nexport type MessageRole = \"system\" | \"user\" | \"assistant\" | \"tool\"\n\nexport interface Message {\n role: MessageRole\n parts: MessagePart[]\n}\n\nconst ALLOWED_ROLES: ReadonlySet<MessageRole> = new Set([\"system\", \"user\", \"assistant\", \"tool\"])\n\n/**\n * Normalize a single message of any of the provider shapes we know about.\n * Returns `undefined` for non-objects so the caller can skip them.\n */\nexport function normalizeMessage(raw: unknown): Message | undefined {\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n const role = coerceRole(obj.role)\n\n // Pre-normalized: already has a parts array.\n if (Array.isArray(obj.parts)) {\n const parts: MessagePart[] = []\n for (const p of obj.parts) {\n if (p && typeof p === \"object\") parts.push(p as MessagePart)\n }\n return { role, parts: parts.length > 0 ? parts : [{ type: \"text\", content: safeJson(raw) }] }\n }\n\n const content = obj.content ?? obj.text ?? obj.message\n\n // OpenAI tool message: `{role: \"tool\", tool_call_id, content}` — handle\n // before the generic string-content branch so we emit a tool_call_response\n // part rather than a plain text part.\n if (role === \"tool\" && obj.tool_call_id !== undefined) {\n return {\n role,\n parts: [\n {\n type: \"tool_call_response\",\n id: typeof obj.tool_call_id === \"string\" ? obj.tool_call_id : \"\",\n response: content ?? safeJson(obj),\n },\n ],\n }\n }\n\n if (typeof content === \"string\") {\n const parts: MessagePart[] = [{ type: \"text\", content }]\n // OpenAI assistant messages may have tool_calls alongside string content.\n appendToolCalls(parts, obj.tool_calls)\n return { role, parts }\n }\n\n if (Array.isArray(content)) {\n const parts: MessagePart[] = []\n for (const block of content) {\n const part = normalizeBlock(block)\n if (part) parts.push(part)\n }\n appendToolCalls(parts, obj.tool_calls)\n if (parts.length === 0) parts.push({ type: \"text\", content: safeJson(content) })\n return { role, parts }\n }\n\n // Unknown shape — dump as JSON so nothing is silently dropped.\n return { role, parts: [{ type: \"text\", content: safeJson(raw) }] }\n}\n\n/** Normalize an array of provider messages. */\nexport function normalizeMessages(raw: unknown[]): Message[] {\n const out: Message[] = []\n for (const m of raw) {\n const norm = normalizeMessage(m)\n if (norm) out.push(norm)\n }\n return out\n}\n\n/** Build a single user message from a string prompt. */\nexport function userMessageFromPrompt(prompt: string): Message {\n return { role: \"user\", parts: [{ type: \"text\", content: prompt }] }\n}\n\n/** Build a single assistant message from `assistantTexts` + `lastAssistant` fallback. */\nexport function assistantMessageFromOutput(assistantTexts: string[], lastAssistant: unknown): Message {\n if (lastAssistant !== undefined) {\n const norm = normalizeMessage(lastAssistant)\n if (norm) return { ...norm, role: \"assistant\" }\n }\n const parts: MessagePart[] = []\n for (const text of assistantTexts) {\n if (text.length > 0) parts.push({ type: \"text\", content: text })\n }\n if (parts.length === 0) parts.push({ type: \"text\", content: \"\" })\n return { role: \"assistant\", parts }\n}\n\n/**\n * Wrap a system prompt string into the parts-array shape expected for\n * `gen_ai.system_instructions`. Empty string in → single empty text part out\n * (still a valid array, never `undefined`).\n */\nexport function systemInstructionsParts(prompt: string): MessagePart[] {\n return [{ type: \"text\", content: prompt }]\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction coerceRole(raw: unknown): MessageRole {\n if (typeof raw !== \"string\") return \"user\"\n return ALLOWED_ROLES.has(raw as MessageRole) ? (raw as MessageRole) : \"user\"\n}\n\nfunction normalizeBlock(raw: unknown): MessagePart | undefined {\n if (typeof raw === \"string\") return { type: \"text\", content: raw }\n if (!raw || typeof raw !== \"object\") return undefined\n const obj = raw as Record<string, unknown>\n\n // Already a part.\n if (typeof obj.type === \"string\" && (typeof obj.content === \"string\" || obj.content === undefined)) {\n // If it carries our recognized shape (text / tool_call / tool_call_response /\n // uri), pass through. Otherwise fall through to type-specific normalization.\n if (obj.type === \"text\" && typeof obj.content === \"string\") {\n return { type: \"text\", content: obj.content }\n }\n }\n\n const type = typeof obj.type === \"string\" ? obj.type : \"text\"\n\n if (type === \"text\" && typeof obj.text === \"string\") {\n return { type: \"text\", content: obj.text }\n }\n if (type === \"tool_use\") {\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.input ?? {},\n }\n }\n if (type === \"tool_call\") {\n // Already-normalized tool_call part — pass through.\n return {\n type: \"tool_call\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n name: typeof obj.name === \"string\" ? obj.name : \"\",\n arguments: obj.arguments ?? obj.input ?? {},\n }\n }\n if (type === \"tool_result\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.tool_use_id === \"string\" ? obj.tool_use_id : \"\",\n response: obj.content ?? \"\",\n }\n }\n if (type === \"tool_call_response\") {\n return {\n type: \"tool_call_response\",\n id: typeof obj.id === \"string\" ? obj.id : \"\",\n response: obj.response ?? \"\",\n }\n }\n if (type === \"thinking\" && typeof obj.thinking === \"string\") {\n return { type: \"reasoning\", content: obj.thinking }\n }\n if (type === \"reasoning\" && typeof obj.content === \"string\") {\n return { type: \"reasoning\", content: obj.content }\n }\n if (type === \"image\" && obj.source && typeof obj.source === \"object\") {\n const src = obj.source as { media_type?: string; data?: string; url?: string }\n const uri = src.url ?? (src.data ? `data:${src.media_type ?? \"image/unknown\"};base64,${src.data}` : \"\")\n if (uri) return { type: \"uri\", modality: \"image\", uri }\n }\n\n // Unknown block type — stringify so nothing is silently dropped.\n return { type, content: safeJson(raw) }\n}\n\n/**\n * OpenAI assistant messages put tool calls in a separate `tool_calls` array\n * alongside string content. Append them as parts so the trace shows what the\n * model emitted in that turn.\n */\nfunction appendToolCalls(parts: MessagePart[], raw: unknown): void {\n if (!Array.isArray(raw)) return\n for (const tc of raw) {\n if (!tc || typeof tc !== \"object\") continue\n const t = tc as Record<string, unknown>\n const fn = t.function as { name?: string; arguments?: string | Record<string, unknown> } | undefined\n let parsedArgs: unknown = fn?.arguments\n if (typeof parsedArgs === \"string\") {\n try {\n parsedArgs = JSON.parse(parsedArgs)\n } catch {\n // leave as string\n }\n }\n parts.push({\n type: \"tool_call\",\n id: typeof t.id === \"string\" ? t.id : \"\",\n name: fn?.name ?? \"\",\n arguments: parsedArgs ?? {},\n })\n }\n}\n\nfunction safeJson(value: unknown): string {\n try {\n if (typeof value === \"string\") return value\n return JSON.stringify(value)\n } catch {\n return \"\"\n }\n}\n","import { createHash, randomUUID } from \"node:crypto\"\nimport {\n assistantMessageFromOutput,\n type Message,\n normalizeMessages,\n systemInstructionsParts,\n userMessageFromPrompt,\n} from \"./messages.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawLlmUsage,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Builds the per-trace span tree for an OpenClaw agent run from the granular\n * paired hooks. Replaces the older `turn-builder.ts` model that collapsed the\n * whole attempt into a single `llm_request` span — that shape was wrong on\n * two counts: `llm_input` / `llm_output` fire ONCE per attempt (not per\n * generation), and an attempt is a sequence of generations interleaved with\n * tool executions.\n *\n * Span set this builder produces:\n *\n * agent (root)\n * ├─ compaction (0..1, rare)\n * ├─ model_call (1..N, one per provider API call)\n * ├─ tool_call: ... (interleaved between model_calls; siblings of agent)\n * ├─ subagent (0..N; child agent runs nest INSIDE these via\n * │ └─ agent ... cross-runId trace propagation)\n * └─ model_call (final)\n *\n * Tool spans are siblings of `agent`, not children of `model_call`, because\n * tools run BETWEEN generations — not during them. Nesting under model_call\n * would falsely imply concurrency.\n *\n * `llm_input` / `llm_output` are NOT span boundaries here. They're data-only\n * feeds that enrich the parent `agent` span (full message history, output\n * messages, aggregate token usage).\n */\n\n// ─── Span record shapes ─────────────────────────────────────────────────────\n\nexport interface SpanRecord {\n /** Stable id for the span (16 hex chars). */\n spanId: string\n /** Span tree id (32 hex chars). */\n traceId: string\n /** Empty string for root agent spans, parent's spanId otherwise. */\n parentSpanId: string\n /** OpenClaw event noun (`agent` / `model_call` / `tool_call` / `compaction` / `subagent`). */\n name: string\n startMs: number\n endMs: number | undefined\n /** Free-form attribute bag — flattened to OTLP key/value at emit time. */\n attrs: Record<string, AttrValue>\n /** Status — set at close from the event payload's outcome/error. */\n outcome?: \"ok\" | \"error\"\n errorMessage?: string | undefined\n}\n\nexport type AttrValue = string | number | boolean | unknown[] | Record<string, unknown> | undefined\n\n// One entry per simple `prefix.field` attribute the builder records on a span.\ntype AttrInput = Record<string, AttrValue>\n\n// ─── Per-run state ──────────────────────────────────────────────────────────\n\ninterface RunState {\n /** Trace root span (the `agent`). */\n agent: SpanRecord\n /**\n * Working snapshot of conversation history in the parts-based GenAI shape\n * Latitude's parser expects. Provider-specific shapes from `llm_input` get\n * normalized once on entry; tool_call / tool_call_response parts appended\n * during the run are already in the right shape.\n */\n history: Message[]\n /** Open per-call spans, keyed on the OpenClaw `callId` from `model_call_started`. */\n openModelCalls: Map<string, SpanRecord>\n /** Open tool spans, keyed on `toolCallId`. */\n openToolCalls: Map<string, SpanRecord>\n /** Open compaction span (at most one in flight). */\n openCompaction: SpanRecord | undefined\n /** All closed spans for this run, ready to emit on agent_end. */\n closed: SpanRecord[]\n /** Any subagent spans we OPENED inside this run, keyed by child runId so we\n * can close them when the child's `subagent_ended` arrives. */\n childSubagentSpans: Map<string, SpanRecord>\n}\n\n/**\n * When a parent's `subagent_spawned` fires we register a link from the\n * child's runId → the parent's traceId + the subagent span's id. Then when the\n * child's `before_agent_start` fires, we use those values so the child's\n * entire span subtree lands inside the parent's trace. Outlives the parent's\n * RunState because the child's `agent_end` may arrive after the parent's.\n *\n * `createdAt` is used by the `evictStaleSubagentLinks` sweep to drop entries\n * whose child runs never reached `agent_end` (gateway crash mid-spawn,\n * plugin reload, etc.). Without the sweep the map grows unbounded over the\n * lifetime of a long-running OpenClaw process.\n */\ninterface SubagentLink {\n traceId: string\n /** Span id of the parent's `subagent` span — used as parentSpanId for the child's root. */\n subagentSpanId: string\n createdAt: number\n}\n\nconst SUBAGENT_LINK_TTL_MS = 60 * 60 * 1000 // 1 hour\nconst SUBAGENT_LINK_MAX = 1000\n\nexport interface BuildResult {\n /** Run id this batch belongs to. */\n runId: string\n /** All spans ready to be exported (agent + everything beneath it). */\n spans: SpanRecord[]\n}\n\n// ─── Builder ────────────────────────────────────────────────────────────────\n\nexport class SpanBuilder {\n private readonly runs = new Map<string, RunState>()\n private readonly subagentLinks = new Map<string, SubagentLink>()\n\n inflightCount(): number {\n return this.runs.size\n }\n\n /**\n * Open the root `agent` span. If the runId was previously registered as a\n * subagent's child, propagate the parent's traceId and parent the new span\n * under the parent's `subagent` span — so the entire subagent's work nests\n * inside the parent's trace as one waterfall.\n */\n onBeforeAgentStart(evt: OpenClawBeforeAgentStartEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n if (this.runs.has(runId)) return // defensive — already open\n\n const link = this.subagentLinks.get(runId)\n const traceId = link?.traceId ?? hashHex(runId, 32)\n const parentSpanId = link?.subagentSpanId ?? \"\"\n\n const agent: SpanRecord = {\n spanId: hashHex(`${traceId}:${runId}:agent`, 16),\n traceId,\n parentSpanId,\n name: \"agent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...flattenCtx(ctx),\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": runId,\n // before_agent_start payload — gated content fields go via `gated.*`\n // attribute keys so the OTLP layer can scrub them without a parallel\n // boolean check. Messages get normalized to parts-shape; `prompt` is\n // a plain string (for the user-prompt convenience attribute).\n \"before_agent_start.prompt:gated\": evt.prompt,\n \"before_agent_start.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n\n this.runs.set(runId, {\n agent,\n history: [],\n openModelCalls: new Map(),\n openToolCalls: new Map(),\n openCompaction: undefined,\n closed: [],\n childSubagentSpans: new Map(),\n })\n }\n\n /**\n * Enrich the open `agent` span with content + identity from the LLM input.\n * Also seeds the rolling history snapshot used by per-call `model_call`\n * input attributes.\n *\n * Provider-specific message shapes get normalized into the parts-based\n * GenAI format here — that's the contract Latitude's downstream parser\n * expects on `gen_ai.input.messages` and `gen_ai.system_instructions`.\n */\n onLlmInput(evt: OpenClawLlmInputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n\n const normalizedHistory = normalizeMessages(evt.historyMessages)\n const inputMessages: Message[] = [...normalizedHistory]\n if (evt.prompt) inputMessages.push(userMessageFromPrompt(evt.prompt))\n\n Object.assign(run.agent.attrs, {\n \"gen_ai.system_instructions:gated\": evt.systemPrompt ? systemInstructionsParts(evt.systemPrompt) : undefined,\n \"user_prompt:gated\": evt.prompt,\n \"gen_ai.input.messages:gated\": inputMessages,\n \"openclaw.images.count\": evt.imagesCount,\n \"gen_ai.request.model\": evt.model,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n })\n // Seed the rolling history with the normalized form. The per-call\n // model_call_started will copy the snapshot at the time it fires;\n // subsequent before_tool_call / after_tool_call events append to the\n // same array (already in parts shape) so the next model_call captures\n // the post-tool state.\n run.history = inputMessages\n }\n\n /**\n * Enrich the agent span with attempt-aggregate output + token usage.\n * (Per-call usage isn't surfaced by OpenClaw today — see PR #2986.)\n */\n onLlmOutput(evt: OpenClawLlmOutputEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(ctx.runId ?? evt.runId)\n if (!run) return\n const assistantMessage = assistantMessageFromOutput(evt.assistantTexts, evt.lastAssistant)\n Object.assign(run.agent.attrs, {\n \"gen_ai.output.messages:gated\": [assistantMessage],\n \"openclaw.resolved.ref\": evt.resolvedRef,\n \"openclaw.harness.id\": evt.harnessId,\n \"gen_ai.response.model\": evt.model,\n ...usageAttrs(evt.usage),\n })\n }\n\n onModelCallStarted(evt: OpenClawModelCallStartedEvent, ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:model_call:${evt.callId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"model_call\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"openclaw.call.id\": evt.callId,\n \"gen_ai.system\": evt.provider,\n \"openclaw.provider\": evt.provider,\n \"gen_ai.request.model\": evt.model,\n \"openclaw.api\": evt.api,\n \"openclaw.transport\": evt.transport,\n // Snapshot the rolling history at the moment this generation starts.\n // The model saw exactly this state. Gated.\n \"gen_ai.input.messages:gated\": [...run.history],\n },\n }\n run.openModelCalls.set(evt.callId, span)\n }\n\n onModelCallEnded(evt: OpenClawModelCallEndedEvent, _ctx: OpenClawAgentContext): void {\n const run = this.runs.get(evt.runId)\n if (!run) return\n const span = run.openModelCalls.get(evt.callId)\n if (!span) return\n span.endMs = Date.now()\n span.outcome = evt.outcome === \"completed\" ? \"ok\" : \"error\"\n span.errorMessage = evt.errorCategory\n Object.assign(span.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.outcome\": evt.outcome,\n \"openclaw.error.category\": evt.errorCategory,\n \"openclaw.failure.kind\": evt.failureKind,\n \"openclaw.request.payload_bytes\": evt.requestPayloadBytes,\n \"openclaw.response.stream_bytes\": evt.responseStreamBytes,\n \"openclaw.ttfb_ms\": evt.timeToFirstByteMs,\n \"openclaw.upstream.request_id_hash\": evt.upstreamRequestIdHash,\n })\n run.openModelCalls.delete(evt.callId)\n run.closed.push(span)\n }\n\n /**\n * Open a `tool_call` span as a sibling of the agent span. Also append a\n * synthetic assistant `tool_call` part to the rolling history so the NEXT\n * model_call's input snapshot reflects what the model emitted.\n *\n * IMPORTANT: this runs as a `runModifyingHook` in OpenClaw — returning\n * anything other than `undefined`/falsy from this handler blocks the tool.\n * The plugin-side handler enforces a void return; this method's signature\n * already returns `void`.\n */\n onBeforeToolCall(evt: OpenClawBeforeToolCallEvent, ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n const toolCallId = evt.toolCallId ?? `${evt.toolName}:${randomUUID()}`\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:tool_call:${toolCallId}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: `tool_call:${evt.toolName}`,\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": evt.runId,\n \"gen_ai.tool.name\": evt.toolName,\n \"gen_ai.tool.call.id\": toolCallId,\n \"gen_ai.tool.call.arguments:gated\": evt.params,\n },\n }\n run.openToolCalls.set(toolCallId, span)\n\n // Append an assistant tool_call part to the rolling history so the next\n // model_call captures it.\n run.history.push({\n role: \"assistant\",\n parts: [{ type: \"tool_call\", id: toolCallId, name: evt.toolName, arguments: evt.params }],\n })\n }\n\n onAfterToolCall(evt: OpenClawAfterToolCallEvent, _ctx: OpenClawAgentContext): void {\n if (!evt.runId) return\n const run = this.runs.get(evt.runId)\n if (!run) return\n\n // Match priority:\n // 1. Direct id lookup — the happy path.\n // 2. Name-match fallback — if `evt.toolCallId` is missing OR refers to\n // an id we didn't open (e.g. before_tool_call elided it and we\n // synthesised one, then after_tool_call provided the real one).\n //\n // Without the id-mismatch fallback, the open span would never close\n // and would get force-closed as `abandoned` at agent_end.\n let resolvedId: string | undefined =\n evt.toolCallId && run.openToolCalls.has(evt.toolCallId) ? evt.toolCallId : undefined\n if (!resolvedId) resolvedId = this.findOpenToolCallByName(run, evt.toolName)\n if (!resolvedId) return\n const span = run.openToolCalls.get(resolvedId)\n if (!span) return\n const toolCallId: string = resolvedId\n\n span.endMs = Date.now()\n const isError = Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"gen_ai.tool.call.result:gated\": evt.result,\n \"openclaw.error.message:gated\": evt.error,\n \"openclaw.duration_ms\": evt.durationMs,\n })\n run.openToolCalls.delete(toolCallId)\n run.closed.push(span)\n\n // Append the tool response to the rolling history so the next\n // model_call's input snapshot includes it.\n run.history.push({\n role: \"tool\",\n parts: [{ type: \"tool_call_response\", id: toolCallId, response: evt.result ?? evt.error ?? \"\" }],\n })\n }\n\n onBeforeCompaction(evt: OpenClawBeforeCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run) return\n const span: SpanRecord = {\n spanId: hashHex(`${run.agent.traceId}:compaction:${run.closed.length}`, 16),\n traceId: run.agent.traceId,\n parentSpanId: run.agent.spanId,\n name: \"compaction\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n ...latitudeAttrs(ctx),\n \"openclaw.run.id\": runId,\n \"openclaw.compaction.message_count.before\": evt.messageCount,\n \"openclaw.compaction.session_file\": evt.sessionFile,\n \"before_compaction.messages:gated\": evt.messages ? normalizeMessages(evt.messages) : undefined,\n },\n }\n run.openCompaction = span\n }\n\n onAfterCompaction(evt: OpenClawAfterCompactionEvent, ctx: OpenClawAgentContext): void {\n const runId = ctx.runId\n if (!runId) return\n const run = this.runs.get(runId)\n if (!run?.openCompaction) return\n const span = run.openCompaction\n span.endMs = Date.now()\n span.outcome = \"ok\"\n Object.assign(span.attrs, {\n \"openclaw.compaction.message_count.after\": evt.messageCount,\n \"openclaw.compaction.compacted_count\": evt.compactedCount,\n \"openclaw.compaction.token_count\": evt.tokenCount,\n })\n run.openCompaction = undefined\n run.closed.push(span)\n }\n\n /**\n * Open a `subagent` span on the parent run AND register the cross-run link\n * so the child's `before_agent_start` can find us.\n */\n onSubagentSpawned(evt: OpenClawSubagentSpawnedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n\n const span: SpanRecord = {\n spanId: hashHex(`${parent.agent.traceId}:subagent:${evt.runId}`, 16),\n traceId: parent.agent.traceId,\n parentSpanId: parent.agent.spanId,\n name: \"subagent\",\n startMs: Date.now(),\n endMs: undefined,\n attrs: {\n // The subagent span lives inside the parent's trace — tags +\n // metadata reflect the parent's ctx (which agent/channel/trigger\n // is doing the spawning), not the child's. The child's `agent` span\n // (opened later by the child run's before_agent_start) carries\n // its OWN ctx.\n ...latitudeAttrs(ctx),\n \"openclaw.parent.run.id\": parentRunId,\n \"openclaw.run.id\": evt.runId,\n \"openclaw.subagent.child_session_key\": evt.childSessionKey,\n \"openclaw.subagent.agent_id\": evt.agentId,\n \"openclaw.subagent.label\": evt.label,\n \"openclaw.subagent.mode\": evt.mode,\n \"openclaw.subagent.thread_requested\": evt.threadRequested,\n \"openclaw.subagent.requester.channel\": evt.requester?.channel,\n \"openclaw.subagent.requester.account_id\": evt.requester?.accountId,\n \"openclaw.subagent.requester.to\": evt.requester?.to,\n \"openclaw.subagent.requester.thread_id\":\n typeof evt.requester?.threadId === \"string\" || typeof evt.requester?.threadId === \"number\"\n ? String(evt.requester.threadId)\n : undefined,\n },\n }\n parent.childSubagentSpans.set(evt.runId, span)\n this.evictStaleSubagentLinks()\n this.subagentLinks.set(evt.runId, {\n traceId: parent.agent.traceId,\n subagentSpanId: span.spanId,\n createdAt: Date.now(),\n })\n }\n\n onSubagentEnded(evt: OpenClawSubagentEndedEvent, ctx: OpenClawAgentContext): void {\n const parentRunId = ctx.runId\n if (!parentRunId) return\n const parent = this.runs.get(parentRunId)\n if (!parent) return\n const childRunId = evt.runId\n if (!childRunId) return\n const span = parent.childSubagentSpans.get(childRunId)\n if (!span) return\n\n span.endMs = Date.now()\n const isError = evt.outcome === \"error\" || Boolean(evt.error)\n span.outcome = isError ? \"error\" : \"ok\"\n span.errorMessage = evt.error\n Object.assign(span.attrs, {\n \"openclaw.subagent.target_session_key\": evt.targetSessionKey,\n \"openclaw.subagent.target_kind\": evt.targetKind,\n \"openclaw.subagent.reason\": evt.reason,\n \"openclaw.subagent.outcome\": evt.outcome,\n \"openclaw.subagent.send_farewell\": evt.sendFarewell,\n \"openclaw.subagent.account_id\": evt.accountId,\n })\n parent.childSubagentSpans.delete(childRunId)\n parent.closed.push(span)\n // Don't delete the subagent link yet — the child's `agent_end` may still\n // be in flight. We clean it up when the child's agent_end fires.\n }\n\n /**\n * Close out the run: finish the agent span, abandon any still-open\n * model_calls / tool_calls / compactions, and return everything ready to\n * emit. Removes the subagent link if this was a child run.\n */\n onAgentEnd(evt: OpenClawAgentEndEvent, ctx: OpenClawAgentContext): BuildResult | undefined {\n const runId = ctx.runId\n if (!runId) return undefined\n const run = this.runs.get(runId)\n if (!run) {\n // Child agent run that closed without ever opening — drop the link\n // entry so the map doesn't grow unbounded.\n this.subagentLinks.delete(runId)\n return undefined\n }\n\n const now = Date.now()\n run.agent.endMs = now\n run.agent.outcome = evt.success ? \"ok\" : \"error\"\n run.agent.errorMessage = evt.error\n Object.assign(run.agent.attrs, {\n \"openclaw.duration_ms\": evt.durationMs,\n \"openclaw.run.success\": evt.success,\n \"openclaw.error.message:gated\": evt.error,\n \"agent_end.messages:gated\": normalizeMessages(evt.messages),\n })\n\n // Anything still open at agent_end didn't get a proper close event.\n // Mark them abandoned and force-close so they show up in the trace\n // rather than vanish silently.\n for (const span of run.openModelCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n for (const span of run.openToolCalls.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n if (run.openCompaction) {\n run.openCompaction.endMs = now\n run.openCompaction.outcome = \"error\"\n run.openCompaction.attrs[\"openclaw.outcome\"] = \"abandoned\"\n run.closed.push(run.openCompaction)\n }\n for (const span of run.childSubagentSpans.values()) {\n span.endMs = now\n span.outcome = \"error\"\n span.attrs[\"openclaw.subagent.outcome\"] = \"abandoned\"\n run.closed.push(span)\n }\n\n const spans = [run.agent, ...run.closed]\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n return { runId, spans }\n }\n\n /** Drop a run without emitting — used on errors from the emit path. */\n abandon(runId: string): void {\n this.runs.delete(runId)\n this.subagentLinks.delete(runId)\n }\n\n /** Test-only: how many cross-run subagent links we're holding. */\n subagentLinkCount(): number {\n return this.subagentLinks.size\n }\n\n /**\n * Drop any subagent links whose child run never reached `agent_end`. Called\n * before every `subagent_spawned` insert so the map stays bounded even when\n * children crash mid-spawn or the plugin reloads.\n *\n * Two passes: TTL eviction (anything older than `SUBAGENT_LINK_TTL_MS`),\n * then a hard size cap (when we're past `SUBAGENT_LINK_MAX`, drop the\n * oldest until we're under).\n */\n private evictStaleSubagentLinks(): void {\n const now = Date.now()\n for (const [runId, link] of this.subagentLinks) {\n if (now - link.createdAt > SUBAGENT_LINK_TTL_MS) {\n this.subagentLinks.delete(runId)\n }\n }\n if (this.subagentLinks.size <= SUBAGENT_LINK_MAX) return\n const sorted = Array.from(this.subagentLinks.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt)\n const toRemove = this.subagentLinks.size - SUBAGENT_LINK_MAX\n for (let i = 0; i < toRemove; i++) {\n const entry = sorted[i]\n if (entry) this.subagentLinks.delete(entry[0])\n }\n }\n\n private findOpenToolCallByName(run: RunState, toolName: string): string | undefined {\n // Defensive: OpenClaw versions that elide toolCallId on after_tool_call\n // can be matched by name + still-open status. When multiple in-flight\n // tool calls share a name, prefer the MOST RECENTLY opened — Maps\n // preserve insertion order, so iterating in reverse picks the latest.\n // (LIFO matches typical agent runtimes that issue tools sequentially.)\n const target = `tool_call:${toolName}`\n const entries = Array.from(run.openToolCalls.entries())\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i]\n if (!entry) continue\n const [id, span] = entry\n if (span.name === target) return id\n }\n return undefined\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction flattenCtx(ctx: OpenClawAgentContext): AttrInput {\n return {\n \"openclaw.run.id\": ctx.runId,\n \"openclaw.session.id\": ctx.sessionId,\n \"openclaw.session.key\": ctx.sessionKey,\n \"openclaw.agent.id\": ctx.agentId,\n \"openclaw.agent.name\": ctx.agentId,\n \"openclaw.workspace.dir\": ctx.workspaceDir,\n \"openclaw.message.provider\": ctx.messageProvider,\n \"openclaw.trigger\": ctx.trigger,\n \"openclaw.channel.id\": ctx.channelId,\n \"openclaw.cron.job.id\": ctx.jobId,\n \"openclaw.model.provider.id\": ctx.modelProviderId,\n \"openclaw.model.id\": ctx.modelId,\n }\n}\n\n/**\n * Build `latitude.tags` and `latitude.metadata` attrs from the hook context.\n * The OTLP encoder JSON-stringifies arrays/objects, which is the encoding\n * Latitude's resolver expects:\n *\n * - `latitude.tags` is a JSON-encoded string array (`fromJsonStringArray`\n * in domain/spans/src/otlp/resolvers/enrichment.ts).\n * - `latitude.metadata` is a JSON-encoded string object (`fromJsonString`).\n *\n * Tags = the agent id, the channel id, and the trigger. When trigger is\n * `cron`, the tag becomes `cron:<jobId>` so dashboards can pivot on the\n * specific cron job. Each tag is conditionally included so absent ctx\n * fields don't produce empty entries.\n *\n * Metadata = every ctx field that's set, namespaced under `openclaw.*` so\n * it can't collide with metadata keys other plugins might emit.\n */\nfunction latitudeAttrs(ctx: OpenClawAgentContext): AttrInput {\n const tags: string[] = []\n if (ctx.agentId) tags.push(ctx.agentId)\n if (ctx.channelId) tags.push(ctx.channelId)\n if (ctx.trigger) {\n tags.push(ctx.trigger === \"cron\" && ctx.jobId ? `cron:${ctx.jobId}` : ctx.trigger)\n }\n\n const metadata: Record<string, string> = {}\n if (ctx.runId) metadata[\"openclaw.run.id\"] = ctx.runId\n if (ctx.sessionId) metadata[\"openclaw.session.id\"] = ctx.sessionId\n if (ctx.sessionKey) metadata[\"openclaw.session.key\"] = ctx.sessionKey\n if (ctx.agentId) metadata[\"openclaw.agent.id\"] = ctx.agentId\n if (ctx.workspaceDir) metadata[\"openclaw.workspace.dir\"] = ctx.workspaceDir\n if (ctx.channelId) metadata[\"openclaw.channel.id\"] = ctx.channelId\n if (ctx.messageProvider) metadata[\"openclaw.message.provider\"] = ctx.messageProvider\n if (ctx.trigger) metadata[\"openclaw.trigger\"] = ctx.trigger\n if (ctx.jobId) metadata[\"openclaw.cron.job.id\"] = ctx.jobId\n if (ctx.modelProviderId) metadata[\"openclaw.model.provider.id\"] = ctx.modelProviderId\n if (ctx.modelId) metadata[\"openclaw.model.id\"] = ctx.modelId\n\n return {\n \"latitude.tags\": tags.length > 0 ? tags : undefined,\n \"latitude.metadata\": Object.keys(metadata).length > 0 ? metadata : undefined,\n }\n}\n\nfunction usageAttrs(usage: OpenClawLlmUsage | undefined): AttrInput {\n if (!usage) return {}\n return {\n \"gen_ai.usage.input_tokens\": usage.input,\n \"gen_ai.usage.output_tokens\": usage.output,\n \"gen_ai.usage.cache_read_input_tokens\": usage.cacheRead,\n \"gen_ai.usage.cache_creation_input_tokens\": usage.cacheWrite,\n \"gen_ai.usage.total_tokens\": usage.total,\n }\n}\n\nfunction hashHex(input: string, length: number): string {\n return createHash(\"sha256\").update(input).digest(\"hex\").slice(0, length)\n}\n","import { postTraces } from \"./client.ts\"\nimport { type Config, loadConfig } from \"./config.ts\"\nimport { createLogger, type Logger } from \"./logger.ts\"\nimport { buildOtlpRequest } from \"./otlp.ts\"\nimport { type BuildResult, SpanBuilder } from \"./span-builder.ts\"\nimport type {\n OpenClawAfterCompactionEvent,\n OpenClawAfterToolCallEvent,\n OpenClawAgentContext,\n OpenClawAgentEndEvent,\n OpenClawBeforeAgentStartEvent,\n OpenClawBeforeCompactionEvent,\n OpenClawBeforeToolCallEvent,\n OpenClawLlmInputEvent,\n OpenClawLlmOutputEvent,\n OpenClawModelCallEndedEvent,\n OpenClawModelCallStartedEvent,\n OpenClawSubagentEndedEvent,\n OpenClawSubagentSpawnedEvent,\n} from \"./types.ts\"\n\n/**\n * Minimal structural type for OpenClaw's plugin API — only the fields we\n * touch. We avoid importing from `openclaw/plugin-sdk` so the package stays\n * usable when OpenClaw isn't installed (the CLI and tests don't need it),\n * and so we're robust to small signature changes across OpenClaw versions.\n *\n * `pluginConfig` is the user's `plugins.entries[id].config` block — that's\n * the canonical place to read credentials and feature flags. The OpenClaw\n * plugin SDK also exposes the same value as `api.pluginConfig` on the\n * builder API; keep both names in sync if the upstream contract evolves.\n */\nexport interface OpenClawPluginApiLike {\n logger?: Logger\n pluginConfig?: Record<string, unknown>\n on: <K extends string>(\n hookName: K,\n handler: (event: unknown, ctx: unknown) => unknown,\n opts?: { priority?: number },\n ) => void\n}\n\nexport interface RegisterOptions {\n /** Override the config, mostly for tests. */\n config?: Config\n /** Override the logger. */\n logger?: Logger\n /**\n * Hook to observe the emitted run right before it's posted. Used by tests;\n * not a stable public API.\n */\n onEmit?: (result: BuildResult) => void\n}\n\n/**\n * Register the Latitude plugin against an OpenClaw plugin API. OpenClaw calls\n * this once at plugin activation; we wire up the granular paired hooks\n * (model_call_started/_ended, before_/after_tool_call, before_/after_compaction,\n * subagent_spawned/_ended, before_agent_start/agent_end) plus the\n * data-only feeds (llm_input/llm_output) that enrich the agent span.\n *\n * Every typed hook on OpenClaw's side fires fire-and-forget for non-modifying\n * hooks; before_tool_call is a `runModifyingHook` where returning anything\n * other than undefined blocks the tool call. Our handler returns nothing —\n * keep it that way.\n */\nexport default function registerLatitudePlugin(api: OpenClawPluginApiLike, opts: RegisterOptions = {}): void {\n // Source of truth: OpenClaw passes the user's `plugins.entries[id].config`\n // as `api.pluginConfig`. Env vars are a fallback so existing deploys with\n // LATITUDE_* exported in the gateway environment keep working.\n const config = opts.config ?? loadConfig(api.pluginConfig)\n const logger = opts.logger ?? createLogger(config.debug)\n\n if (!config.enabled) {\n if (config.apiKey === \"\") logger.debug(\"disabled: apiKey is empty (set plugins.entries[id].config.apiKey)\")\n if (config.project === \"\") logger.debug(\"disabled: project is empty (set plugins.entries[id].config.project)\")\n return\n }\n logger.debug(\n `enabled: project=${config.project} base=${config.baseUrl} allowConversationAccess=${config.allowConversationAccess}`,\n )\n\n const builder = new SpanBuilder()\n\n // Helper: wrap a void-returning hook handler with try/catch + cast.\n const wrap = <E>(\n name: string,\n fn: (evt: E, ctx: OpenClawAgentContext) => void,\n ): ((evt: unknown, ctx: unknown) => void) => {\n return (evt, ctx) => {\n try {\n fn(evt as E, ctx as OpenClawAgentContext)\n } catch (err) {\n logger.warn(`${name} handler failed: ${String(err)}`)\n }\n }\n }\n\n // ─── Span boundaries ────────────────────────────────────────────────────\n\n api.on(\n \"before_agent_start\",\n wrap<OpenClawBeforeAgentStartEvent>(\"before_agent_start\", (evt, ctx) => {\n builder.onBeforeAgentStart(evt, ctx)\n }),\n )\n\n api.on(\n \"model_call_started\",\n wrap<OpenClawModelCallStartedEvent>(\"model_call_started\", (evt, ctx) => {\n builder.onModelCallStarted(evt, ctx)\n }),\n )\n api.on(\n \"model_call_ended\",\n wrap<OpenClawModelCallEndedEvent>(\"model_call_ended\", (evt, ctx) => {\n builder.onModelCallEnded(evt, ctx)\n }),\n )\n\n // before_tool_call is a `runModifyingHook` — returning {block: true} from\n // any plugin handler blocks the tool. We return nothing (void) so OpenClaw\n // dispatches normally. The `wrap` helper preserves that void return.\n api.on(\n \"before_tool_call\",\n wrap<OpenClawBeforeToolCallEvent>(\"before_tool_call\", (evt, ctx) => {\n builder.onBeforeToolCall(evt, ctx)\n }),\n )\n api.on(\n \"after_tool_call\",\n wrap<OpenClawAfterToolCallEvent>(\"after_tool_call\", (evt, ctx) => {\n builder.onAfterToolCall(evt, ctx)\n }),\n )\n\n api.on(\n \"before_compaction\",\n wrap<OpenClawBeforeCompactionEvent>(\"before_compaction\", (evt, ctx) => {\n builder.onBeforeCompaction(evt, ctx)\n }),\n )\n api.on(\n \"after_compaction\",\n wrap<OpenClawAfterCompactionEvent>(\"after_compaction\", (evt, ctx) => {\n builder.onAfterCompaction(evt, ctx)\n }),\n )\n\n api.on(\n \"subagent_spawned\",\n wrap<OpenClawSubagentSpawnedEvent>(\"subagent_spawned\", (evt, ctx) => {\n builder.onSubagentSpawned(evt, ctx)\n }),\n )\n api.on(\n \"subagent_ended\",\n wrap<OpenClawSubagentEndedEvent>(\"subagent_ended\", (evt, ctx) => {\n builder.onSubagentEnded(evt, ctx)\n }),\n )\n\n // ─── Data-only feeds ────────────────────────────────────────────────────\n // These DON'T open or close spans. They enrich the open `agent` span with\n // attempt-aggregate content + token usage, and seed the rolling history\n // snapshot used by per-call `model_call.input.messages`.\n\n api.on(\n \"llm_input\",\n wrap<OpenClawLlmInputEvent>(\"llm_input\", (evt, ctx) => {\n builder.onLlmInput(evt, ctx)\n }),\n )\n api.on(\n \"llm_output\",\n wrap<OpenClawLlmOutputEvent>(\"llm_output\", (evt, ctx) => {\n builder.onLlmOutput(evt, ctx)\n }),\n )\n\n // ─── Trace flush ────────────────────────────────────────────────────────\n\n api.on(\n \"agent_end\",\n wrap<OpenClawAgentEndEvent>(\"agent_end\", (evt, ctx) => {\n const result = builder.onAgentEnd(evt, ctx)\n if (!result) {\n logger.debug(\"agent_end fired without a matching run in flight\")\n return\n }\n opts.onEmit?.(result)\n const payload = buildOtlpRequest(result, { allowConversationAccess: config.allowConversationAccess })\n void postTraces({\n baseUrl: config.baseUrl,\n apiKey: config.apiKey,\n project: config.project,\n payload,\n logger,\n })\n }),\n )\n}\n"],"mappings":"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"}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "@latitude-data/openclaw-telemetry",
|
|
3
3
|
"name": "Latitude Telemetry",
|
|
4
4
|
"description": "Streams every OpenClaw agent run to Latitude as OTLP traces — full prompt, message history, assistant output, tool I/O, token usage, and agent name.",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.6",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latitude-data/openclaw-telemetry",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "OpenClaw plugin that streams LLM calls, tool executions, and agent runs to Latitude as OTLP traces",
|
|
5
5
|
"author": "Latitude Data SL <hello@latitude.so>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,9 +22,6 @@
|
|
|
22
22
|
"dist",
|
|
23
23
|
"openclaw.plugin.json"
|
|
24
24
|
],
|
|
25
|
-
"bin": {
|
|
26
|
-
"latitude-openclaw": "./dist/cli.js"
|
|
27
|
-
},
|
|
28
25
|
"main": "./dist/plugin.js",
|
|
29
26
|
"types": "./dist/plugin.d.ts",
|
|
30
27
|
"openclaw": {
|
|
@@ -40,16 +37,8 @@
|
|
|
40
37
|
"./plugin": {
|
|
41
38
|
"types": "./dist/plugin.d.ts",
|
|
42
39
|
"default": "./dist/plugin.js"
|
|
43
|
-
},
|
|
44
|
-
"./cli": {
|
|
45
|
-
"types": "./dist/cli.d.ts",
|
|
46
|
-
"default": "./dist/cli.js"
|
|
47
40
|
}
|
|
48
41
|
},
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"@clack/prompts": "^1.2.0",
|
|
51
|
-
"picocolors": "^1.1.1"
|
|
52
|
-
},
|
|
53
42
|
"peerDependencies": {
|
|
54
43
|
"typescript": "^6.0.3"
|
|
55
44
|
},
|
package/dist/cli.d.ts
DELETED