@runtypelabs/openclaw-adapter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.mjs +327 -0
- package/openclaw.plugin.json +7 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Runtype, Inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Runtype telemetry for OpenClaw
|
|
2
|
+
|
|
3
|
+
`@runtypelabs/openclaw-adapter` is a native OpenClaw plugin. It observes agent,
|
|
4
|
+
model, and tool hooks and sends one OTLP/JSON trace per completed OpenClaw turn to
|
|
5
|
+
Runtype. It does not host or change the agent.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Set these environment variables on the OpenClaw Gateway process:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
RUNTYPE_API_KEY=rt_... # Runtype key with TELEMETRY:WRITE
|
|
13
|
+
RUNTYPE_AGENT_ID=agent_... # Saved Runtype agent that owns the run
|
|
14
|
+
RUNTYPE_CAPTURE_CONTENT=true # Optional; needed for eval case capture
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`RUNTYPE_OTLP_URL` optionally overrides the default
|
|
18
|
+
`https://api.runtype.com/v1/otel/v1/traces`. Use it only for a trusted OTLP
|
|
19
|
+
receiver; the adapter sends the telemetry key to that URL.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
openclaw plugins install @runtypelabs/openclaw-adapter
|
|
23
|
+
openclaw plugins enable runtype
|
|
24
|
+
openclaw config set plugins.entries.runtype.hooks.allowConversationAccess true --strict-json
|
|
25
|
+
openclaw plugins inspect runtype --runtime
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Reload the plugin or restart the Gateway if it was running before these changes.
|
|
29
|
+
In an interactive terminal, review OpenClaw's package-source and capability
|
|
30
|
+
prompts before accepting them. Noninteractive installation requires `--force`
|
|
31
|
+
and `--accept-capabilities` after independent review; do not add those flags to
|
|
32
|
+
an unattended setup that has not been reviewed.
|
|
33
|
+
The `allowConversationAccess` grant is required for a non-bundled plugin to
|
|
34
|
+
receive `llm_input`, `llm_output`, and `agent_end` hooks. Review the plugin before
|
|
35
|
+
granting it access to conversations. See [OpenClaw's hook documentation](https://docs.openclaw.ai/plugins/hooks).
|
|
36
|
+
|
|
37
|
+
For direct `OPENAI_API_KEY` use, pin the model's OpenClaw runtime rather than
|
|
38
|
+
letting the host select its Codex runtime. For example, under
|
|
39
|
+
`agents.defaults.models["openai/gpt-4.1-mini"]`, set
|
|
40
|
+
`"agentRuntime": { "id": "openclaw" }`. This controls OpenClaw model routing;
|
|
41
|
+
it is not an adapter setting.
|
|
42
|
+
|
|
43
|
+
## What is captured
|
|
44
|
+
|
|
45
|
+
The adapter emits `invoke_agent` and `chat` spans using GenAI semantic
|
|
46
|
+
conventions, and `execute_tool` spans for completed tool hooks. OpenClaw's
|
|
47
|
+
`llm_input` and `llm_output` hooks describe an entire attempt, not each physical
|
|
48
|
+
provider call, so a `chat` span carries attempt-level model and usage data rather
|
|
49
|
+
than per-call timing or token attribution. Missing attempt usage is not silently
|
|
50
|
+
replaced with zero in the run total. If native provider-call metadata shows a
|
|
51
|
+
fallback across different models, the adapter omits aggregate usage rather
|
|
52
|
+
than pricing it all against one model. Runtype attributes each trace to
|
|
53
|
+
`RUNTYPE_AGENT_ID` and identifies its source as
|
|
54
|
+
`@runtypelabs/openclaw-adapter`.
|
|
55
|
+
|
|
56
|
+
Prompt, answer, system-instruction, and tool payloads are **off by default**.
|
|
57
|
+
Set `RUNTYPE_CAPTURE_CONTENT=true` to include bounded input/output messages
|
|
58
|
+
for eval case capture. The adapter removes values of environment variables
|
|
59
|
+
whose names indicate API keys, tokens, secrets, passwords, or credentials, and
|
|
60
|
+
omits any content value over 32 KiB after redaction. Review what your agent
|
|
61
|
+
handles before enabling content; this is not a general-purpose DLP system.
|
|
62
|
+
OpenClaw may suppress conversation hooks for privacy-sensitive sessions; the
|
|
63
|
+
plugin does not reconstruct that content from other hooks. Such runs can have
|
|
64
|
+
a structural trace without an eval-capturable transcript.
|
|
65
|
+
|
|
66
|
+
## Delivery limits
|
|
67
|
+
|
|
68
|
+
The `agent_end` hook waits up to one second for a pending model or tool
|
|
69
|
+
observation, because OpenClaw can deliver `llm_output` just after `agent_end`.
|
|
70
|
+
It flushes immediately when the pending observation arrives and does not
|
|
71
|
+
invent an output if the grace expires. It then makes one HTTP submission, with
|
|
72
|
+
a 10-second timeout. Telemetry failure does not fail the agent turn. OpenClaw can dispatch
|
|
73
|
+
channel-backed `agent_end` hooks without waiting for them, and this adapter has
|
|
74
|
+
no durable retry queue; process exit or network failure can lose a trace.
|
|
75
|
+
This is observational telemetry, not a control or approval mechanism. Runtype
|
|
76
|
+
cannot re-run telemetry-only agents without a callable external-agent endpoint.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
type HookEvent = Record<string, unknown>;
|
|
3
|
+
type HookContext = Record<string, unknown>;
|
|
4
|
+
type HookHandler = (event: HookEvent, context: HookContext) => void | Promise<void>;
|
|
5
|
+
export interface OpenClawHookApi {
|
|
6
|
+
on(name: string, handler: HookHandler): void;
|
|
7
|
+
logger?: {
|
|
8
|
+
warn(message: string): void;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface OpenClawAdapterConfig {
|
|
12
|
+
agentId: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
otlpUrl: string;
|
|
15
|
+
captureContent?: boolean;
|
|
16
|
+
onDiagnostic?: (code: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function createRuntypeOpenClawPlugin(config: OpenClawAdapterConfig): {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
register(api: OpenClawHookApi): void;
|
|
23
|
+
};
|
|
24
|
+
declare const environmentPlugin: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
register(api: OpenClawHookApi): void;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { environmentPlugin as default };
|
|
32
|
+
export {}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region package.json
|
|
3
|
+
var version = "0.1.0";
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const MAX_CONTENT_BYTES = 32768;
|
|
7
|
+
const ADAPTER_VERSION = version;
|
|
8
|
+
function string(value) {
|
|
9
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
10
|
+
}
|
|
11
|
+
function nativeId(event, context) {
|
|
12
|
+
return string(event.runId) ?? string(context.runId);
|
|
13
|
+
}
|
|
14
|
+
function id(value, bytes) {
|
|
15
|
+
return createHash("sha256").update(value).digest("hex").slice(0, bytes * 2);
|
|
16
|
+
}
|
|
17
|
+
function nanos(milliseconds) {
|
|
18
|
+
return (BigInt(Math.trunc(milliseconds)) * 1000000n).toString();
|
|
19
|
+
}
|
|
20
|
+
function number(value) {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
22
|
+
}
|
|
23
|
+
function otlpAttributes(values) {
|
|
24
|
+
return Object.entries(values).map(([key, value]) => ({
|
|
25
|
+
key,
|
|
26
|
+
value: typeof value === "string" ? { stringValue: value } : typeof value === "boolean" ? { boolValue: value } : Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value }
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function configuredSecrets(apiKey) {
|
|
30
|
+
const secrets = Object.entries(process.env).filter(([name, value]) => /(?:API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/i.test(name) && value && value.length >= 8).map(([, value]) => value);
|
|
31
|
+
secrets.push(apiKey);
|
|
32
|
+
return [...new Set(secrets)].sort((a, b) => b.length - a.length);
|
|
33
|
+
}
|
|
34
|
+
function capture(value, secrets, enabled) {
|
|
35
|
+
if (!enabled || value === void 0 || value === null) return void 0;
|
|
36
|
+
let serialized;
|
|
37
|
+
try {
|
|
38
|
+
serialized = JSON.stringify(value);
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const secret of secrets) serialized = serialized.replaceAll(JSON.stringify(secret).slice(1, -1), "[REDACTED]");
|
|
43
|
+
return Buffer.byteLength(serialized, "utf8") <= MAX_CONTENT_BYTES ? serialized : void 0;
|
|
44
|
+
}
|
|
45
|
+
function message(role, value, secrets, enabled) {
|
|
46
|
+
return typeof value === "string" && value.length > 0 ? capture([{
|
|
47
|
+
role,
|
|
48
|
+
content: value
|
|
49
|
+
}], secrets, enabled) : void 0;
|
|
50
|
+
}
|
|
51
|
+
function assistantFailed(assistant) {
|
|
52
|
+
if (!assistant || typeof assistant !== "object") return false;
|
|
53
|
+
const final = assistant;
|
|
54
|
+
return final.stopReason === "error" || final.isError === true;
|
|
55
|
+
}
|
|
56
|
+
function finalAssistantFailed(messages) {
|
|
57
|
+
if (!Array.isArray(messages)) return false;
|
|
58
|
+
return assistantFailed([...messages].reverse().find((item) => item && typeof item === "object" && item.role === "assistant"));
|
|
59
|
+
}
|
|
60
|
+
function makeSpan(nativeRunId, operation, name, start, end, attributes, statusCode) {
|
|
61
|
+
return {
|
|
62
|
+
traceId: id(`openclaw:${nativeRunId}`, 16),
|
|
63
|
+
spanId: id(`openclaw:${nativeRunId}:${operation}`, 8),
|
|
64
|
+
parentSpanId: id(`openclaw:${nativeRunId}:root`, 8),
|
|
65
|
+
name,
|
|
66
|
+
startTimeUnixNano: nanos(start),
|
|
67
|
+
endTimeUnixNano: nanos(Math.max(start, end)),
|
|
68
|
+
attributes,
|
|
69
|
+
status: { code: statusCode }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function encode(spans, agentId) {
|
|
73
|
+
return JSON.stringify({ resourceSpans: [{
|
|
74
|
+
resource: { attributes: otlpAttributes({
|
|
75
|
+
"runtype.agent.id": agentId,
|
|
76
|
+
"runtype.adapter.name": "@runtypelabs/openclaw-adapter",
|
|
77
|
+
"runtype.adapter.version": ADAPTER_VERSION
|
|
78
|
+
}) },
|
|
79
|
+
scopeSpans: [{
|
|
80
|
+
scope: {
|
|
81
|
+
name: "@runtypelabs/openclaw-adapter",
|
|
82
|
+
version: ADAPTER_VERSION
|
|
83
|
+
},
|
|
84
|
+
spans: spans.map((span) => ({
|
|
85
|
+
traceId: span.traceId,
|
|
86
|
+
spanId: span.spanId,
|
|
87
|
+
...span.parentSpanId ? { parentSpanId: span.parentSpanId } : {},
|
|
88
|
+
name: span.name,
|
|
89
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
90
|
+
endTimeUnixNano: span.endTimeUnixNano,
|
|
91
|
+
attributes: otlpAttributes(span.attributes),
|
|
92
|
+
status: span.status
|
|
93
|
+
}))
|
|
94
|
+
}]
|
|
95
|
+
}] });
|
|
96
|
+
}
|
|
97
|
+
function createRuntypeOpenClawPlugin(config) {
|
|
98
|
+
const active = /* @__PURE__ */ new Map();
|
|
99
|
+
const completed = /* @__PURE__ */ new Set();
|
|
100
|
+
const finishing = /* @__PURE__ */ new Set();
|
|
101
|
+
const secrets = configuredSecrets(config.apiKey);
|
|
102
|
+
const content = config.captureContent === true;
|
|
103
|
+
function run(nativeRunId) {
|
|
104
|
+
let current = active.get(nativeRunId);
|
|
105
|
+
if (current) return current;
|
|
106
|
+
if (active.size >= 1024) active.delete(active.keys().next().value);
|
|
107
|
+
current = {
|
|
108
|
+
startedAt: Date.now(),
|
|
109
|
+
inputs: [],
|
|
110
|
+
tools: /* @__PURE__ */ new Map(),
|
|
111
|
+
spans: [],
|
|
112
|
+
modelRefs: /* @__PURE__ */ new Set(),
|
|
113
|
+
inputTokens: 0,
|
|
114
|
+
outputTokens: 0,
|
|
115
|
+
measuredModels: 0,
|
|
116
|
+
cacheReadTokens: 0,
|
|
117
|
+
cacheWriteTokens: 0,
|
|
118
|
+
cacheReadModels: 0,
|
|
119
|
+
cacheWriteModels: 0
|
|
120
|
+
};
|
|
121
|
+
active.set(nativeRunId, current);
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
function diagnostic(code) {
|
|
125
|
+
try {
|
|
126
|
+
config.onDiagnostic?.(code);
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
function settlePending(current) {
|
|
130
|
+
if (current.inputs.length === 0 && current.tools.size === 0) current.onSettled?.();
|
|
131
|
+
}
|
|
132
|
+
async function deliver(payload) {
|
|
133
|
+
try {
|
|
134
|
+
if (!(await fetch(config.otlpUrl, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
redirect: "manual",
|
|
137
|
+
signal: AbortSignal.timeout(1e4),
|
|
138
|
+
headers: {
|
|
139
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
140
|
+
"content-type": "application/json",
|
|
141
|
+
"x-runtype-agent-id": config.agentId
|
|
142
|
+
},
|
|
143
|
+
body: payload
|
|
144
|
+
})).ok) diagnostic("delivery_failed");
|
|
145
|
+
} catch {
|
|
146
|
+
diagnostic("delivery_failed");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
id: "runtype",
|
|
151
|
+
name: "Runtype Telemetry",
|
|
152
|
+
description: "Exports OpenClaw agent turns to Runtype",
|
|
153
|
+
register(api) {
|
|
154
|
+
api.on("llm_input", (event, context) => {
|
|
155
|
+
const key = nativeId(event, context);
|
|
156
|
+
if (!key || completed.has(key) || finishing.has(key)) return;
|
|
157
|
+
run(key).inputs.push({
|
|
158
|
+
at: Date.now(),
|
|
159
|
+
event
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
api.on("model_call_started", (event, context) => {
|
|
163
|
+
const key = nativeId(event, context);
|
|
164
|
+
const provider = string(event.provider);
|
|
165
|
+
const model = string(event.model);
|
|
166
|
+
if (!key || !provider || !model || completed.has(key)) return;
|
|
167
|
+
run(key).modelRefs.add(`${provider}/${model}`);
|
|
168
|
+
});
|
|
169
|
+
api.on("llm_output", (event, context) => {
|
|
170
|
+
const key = nativeId(event, context);
|
|
171
|
+
if (!key || completed.has(key)) return;
|
|
172
|
+
const current = run(key);
|
|
173
|
+
const input = current.inputs.shift();
|
|
174
|
+
const provider = string(event.provider) ?? string(input?.event.provider) ?? "unknown";
|
|
175
|
+
const model = string(event.model) ?? string(input?.event.model) ?? "unknown";
|
|
176
|
+
const texts = Array.isArray(event.assistantTexts) ? event.assistantTexts.filter((item) => typeof item === "string") : [];
|
|
177
|
+
const inputMessages = message("user", input?.event.prompt, secrets, content);
|
|
178
|
+
const outputMessages = message("assistant", texts.join("\n"), secrets, content);
|
|
179
|
+
const system = capture(input?.event.systemPrompt, secrets, content);
|
|
180
|
+
const usage = event.usage && typeof event.usage === "object" ? event.usage : {};
|
|
181
|
+
const inputTokens = number(usage.input);
|
|
182
|
+
const outputTokens = number(usage.output);
|
|
183
|
+
const cacheReadTokens = number(usage.cacheRead);
|
|
184
|
+
const cacheWriteTokens = number(usage.cacheWrite);
|
|
185
|
+
const inclusiveInputTokens = inputTokens === void 0 && cacheReadTokens === void 0 && cacheWriteTokens === void 0 ? void 0 : (inputTokens ?? 0) + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0);
|
|
186
|
+
const index = current.spans.filter((span) => span.attributes["gen_ai.operation.name"] === "chat").length;
|
|
187
|
+
current.spans.push(makeSpan(key, `chat:${index}`, `chat ${model}`, input?.at ?? Date.now(), Date.now(), {
|
|
188
|
+
"gen_ai.operation.name": "chat",
|
|
189
|
+
"gen_ai.provider.name": provider,
|
|
190
|
+
"gen_ai.request.model": model,
|
|
191
|
+
"runtype.iteration": index,
|
|
192
|
+
...inclusiveInputTokens === void 0 ? {} : { "gen_ai.usage.input_tokens": inclusiveInputTokens },
|
|
193
|
+
...outputTokens === void 0 ? {} : { "gen_ai.usage.output_tokens": outputTokens },
|
|
194
|
+
...cacheReadTokens === void 0 ? {} : { "gen_ai.usage.cache_read.input_tokens": cacheReadTokens },
|
|
195
|
+
...cacheWriteTokens === void 0 ? {} : { "gen_ai.usage.cache_creation.input_tokens": cacheWriteTokens },
|
|
196
|
+
...inputMessages === void 0 ? {} : { "gen_ai.input.messages": inputMessages },
|
|
197
|
+
...outputMessages === void 0 ? {} : { "gen_ai.output.messages": outputMessages },
|
|
198
|
+
...system === void 0 ? {} : { "gen_ai.system_instructions": system }
|
|
199
|
+
}, assistantFailed(event.lastAssistant) ? 2 : 1));
|
|
200
|
+
current.firstInput ??= inputMessages;
|
|
201
|
+
current.lastOutput = outputMessages;
|
|
202
|
+
current.lastModel = model;
|
|
203
|
+
current.lastProvider = provider;
|
|
204
|
+
if (inclusiveInputTokens !== void 0 && outputTokens !== void 0) {
|
|
205
|
+
current.inputTokens += inclusiveInputTokens;
|
|
206
|
+
current.outputTokens += outputTokens;
|
|
207
|
+
current.measuredModels += 1;
|
|
208
|
+
}
|
|
209
|
+
if (cacheReadTokens !== void 0) {
|
|
210
|
+
current.cacheReadTokens += cacheReadTokens;
|
|
211
|
+
current.cacheReadModels += 1;
|
|
212
|
+
}
|
|
213
|
+
if (cacheWriteTokens !== void 0) {
|
|
214
|
+
current.cacheWriteTokens += cacheWriteTokens;
|
|
215
|
+
current.cacheWriteModels += 1;
|
|
216
|
+
}
|
|
217
|
+
settlePending(current);
|
|
218
|
+
});
|
|
219
|
+
api.on("before_tool_call", (event, context) => {
|
|
220
|
+
const key = nativeId(event, context);
|
|
221
|
+
const toolKey = string(event.toolCallId) ?? string(event.toolName);
|
|
222
|
+
if (!key || !toolKey || completed.has(key) || finishing.has(key)) return;
|
|
223
|
+
run(key).tools.set(toolKey, {
|
|
224
|
+
at: Date.now(),
|
|
225
|
+
event
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
api.on("after_tool_call", (event, context) => {
|
|
229
|
+
const key = nativeId(event, context);
|
|
230
|
+
if (!key || completed.has(key)) return;
|
|
231
|
+
const current = run(key);
|
|
232
|
+
const toolKey = string(event.toolCallId) ?? string(event.toolName);
|
|
233
|
+
const before = toolKey ? current.tools.get(toolKey) : void 0;
|
|
234
|
+
if (toolKey) current.tools.delete(toolKey);
|
|
235
|
+
const name = string(event.toolName) ?? string(before?.event.toolName) ?? "unknown";
|
|
236
|
+
const callId = string(event.toolCallId) ?? string(before?.event.toolCallId);
|
|
237
|
+
const args = capture(event.params, secrets, content);
|
|
238
|
+
const result = capture(event.result, secrets, content);
|
|
239
|
+
const index = current.spans.filter((span) => span.attributes["gen_ai.operation.name"] === "execute_tool").length + 1;
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
current.spans.push(makeSpan(key, `tool:${index}`, `execute_tool ${name}`, before?.at ?? Math.max(1, now - (number(event.durationMs) ?? 0)), now, {
|
|
242
|
+
"gen_ai.operation.name": "execute_tool",
|
|
243
|
+
"gen_ai.tool.name": name,
|
|
244
|
+
...callId === void 0 ? {} : { "gen_ai.tool.call.id": callId },
|
|
245
|
+
...args === void 0 ? {} : { "gen_ai.tool.call.arguments": args },
|
|
246
|
+
...result === void 0 ? {} : { "gen_ai.tool.call.result": result }
|
|
247
|
+
}, string(event.error) === void 0 ? 1 : 2));
|
|
248
|
+
settlePending(current);
|
|
249
|
+
});
|
|
250
|
+
api.on("agent_end", async (event, context) => {
|
|
251
|
+
const key = nativeId(event, context);
|
|
252
|
+
if (!key || completed.has(key) || finishing.has(key)) return;
|
|
253
|
+
finishing.add(key);
|
|
254
|
+
const current = run(key);
|
|
255
|
+
const terminalAt = Date.now();
|
|
256
|
+
if (current.inputs.length > 0 || current.tools.size > 0) await new Promise((resolve) => {
|
|
257
|
+
const timer = setTimeout(() => {
|
|
258
|
+
current.onSettled = void 0;
|
|
259
|
+
resolve();
|
|
260
|
+
}, 1e3);
|
|
261
|
+
current.onSettled = () => {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
current.onSettled = void 0;
|
|
264
|
+
resolve();
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
active.delete(key);
|
|
268
|
+
completed.add(key);
|
|
269
|
+
finishing.delete(key);
|
|
270
|
+
if (completed.size > 2048) completed.delete(completed.values().next().value);
|
|
271
|
+
const chatSpans = current.spans.filter((span) => span.attributes["gen_ai.operation.name"] === "chat");
|
|
272
|
+
const usageComplete = current.measuredModels === chatSpans.length && current.modelRefs.size <= 1;
|
|
273
|
+
if (!usageComplete) for (const span of chatSpans) {
|
|
274
|
+
delete span.attributes["gen_ai.usage.input_tokens"];
|
|
275
|
+
delete span.attributes["gen_ai.usage.output_tokens"];
|
|
276
|
+
delete span.attributes["gen_ai.usage.cache_read.input_tokens"];
|
|
277
|
+
delete span.attributes["gen_ai.usage.cache_creation.input_tokens"];
|
|
278
|
+
}
|
|
279
|
+
const now = Math.max(terminalAt, ...current.spans.map((span) => Number(BigInt(span.endTimeUnixNano) / 1000000n)));
|
|
280
|
+
const duration = number(event.durationMs);
|
|
281
|
+
const startedAt = duration === void 0 ? current.startedAt : Math.min(current.startedAt, Math.max(1, terminalAt - duration));
|
|
282
|
+
const success = event.success === true && !finalAssistantFailed(event.messages);
|
|
283
|
+
const finalChat = chatSpans.at(-1);
|
|
284
|
+
if (!success && finalChat?.status?.code === 1 && finalChat.attributes["gen_ai.output.messages"] === void 0 && finalChat.attributes["gen_ai.usage.input_tokens"] === void 0 && finalChat.attributes["gen_ai.usage.output_tokens"] === void 0) finalChat.status = { code: 2 };
|
|
285
|
+
const root = makeSpan(key, "root", "invoke_agent", startedAt, now, {
|
|
286
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
287
|
+
"runtype.stop_reason": success ? "stop" : "error",
|
|
288
|
+
...current.firstInput === void 0 ? {} : { "gen_ai.input.messages": current.firstInput },
|
|
289
|
+
...current.lastOutput === void 0 ? {} : { "gen_ai.output.messages": current.lastOutput },
|
|
290
|
+
...current.lastModel === void 0 ? {} : { "gen_ai.request.model": current.lastModel },
|
|
291
|
+
...current.lastProvider === void 0 ? {} : { "gen_ai.provider.name": current.lastProvider },
|
|
292
|
+
...current.measuredModels === 0 || !usageComplete ? {} : {
|
|
293
|
+
"gen_ai.usage.input_tokens": current.inputTokens,
|
|
294
|
+
"gen_ai.usage.output_tokens": current.outputTokens,
|
|
295
|
+
...current.cacheReadModels === chatSpans.length ? { "gen_ai.usage.cache_read.input_tokens": current.cacheReadTokens } : {},
|
|
296
|
+
...current.cacheWriteModels === chatSpans.length ? { "gen_ai.usage.cache_creation.input_tokens": current.cacheWriteTokens } : {}
|
|
297
|
+
}
|
|
298
|
+
}, success ? 1 : 2);
|
|
299
|
+
delete root.parentSpanId;
|
|
300
|
+
await deliver(encode([...current.spans, root], config.agentId));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const environmentPlugin = {
|
|
306
|
+
id: "runtype",
|
|
307
|
+
name: "Runtype Telemetry",
|
|
308
|
+
description: "Exports OpenClaw agent turns to Runtype",
|
|
309
|
+
register(api) {
|
|
310
|
+
const apiKey = process.env.RUNTYPE_API_KEY;
|
|
311
|
+
const agentId = process.env.RUNTYPE_AGENT_ID;
|
|
312
|
+
if (!apiKey || !agentId) {
|
|
313
|
+
api.logger?.warn("Runtype telemetry disabled: RUNTYPE_API_KEY and RUNTYPE_AGENT_ID are required");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
createRuntypeOpenClawPlugin({
|
|
317
|
+
apiKey,
|
|
318
|
+
agentId,
|
|
319
|
+
otlpUrl: process.env.RUNTYPE_OTLP_URL ?? "https://api.runtype.com/v1/otel/v1/traces",
|
|
320
|
+
captureContent: process.env.RUNTYPE_CAPTURE_CONTENT === "true",
|
|
321
|
+
onDiagnostic: (code) => api.logger?.warn(`Runtype telemetry ${code}`)
|
|
322
|
+
}).register(api);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
//#endregion
|
|
326
|
+
export { createRuntypeOpenClawPlugin, environmentPlugin as default };
|
|
327
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runtypelabs/openclaw-adapter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw run telemetry for Runtype",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"openclaw": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"./dist/index.mjs"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"openclaw.plugin.json",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^25.3.3",
|
|
28
|
+
"tsdown": "^0.23.0",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "^5.0.2"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/runtypelabs/core",
|
|
36
|
+
"directory": "packages/openclaw-adapter"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.tests.json",
|
|
42
|
+
"lint": "eslint . --max-warnings 0"
|
|
43
|
+
}
|
|
44
|
+
}
|