@contextcompany/openclaw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/index.cjs +151 -0
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +146 -0
- package/openclaw.plugin.json +36 -0
- package/package.json +60 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +202 -0
- package/src/transport.ts +49 -0
- package/src/types.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @contextcompany/openclaw
|
|
2
|
+
|
|
3
|
+
The Context Company observability plugin for [OpenClaw](https://openclaw.ai).
|
|
4
|
+
|
|
5
|
+
Captures LLM calls, tool executions, and agent lifecycle events from OpenClaw's plugin hook system, then exports them to The Context Company for visualization and analysis.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
openclaw plugins install @contextcompany/openclaw
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. Configure
|
|
16
|
+
|
|
17
|
+
Add to your `openclaw.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"plugins": {
|
|
22
|
+
"allow": ["@contextcompany/openclaw"],
|
|
23
|
+
"entries": {
|
|
24
|
+
"@contextcompany/openclaw": {
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"config": {
|
|
27
|
+
"apiKey": "${TCC_API_KEY}"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Restart
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
openclaw gateway restart
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. The plugin hooks into the agent runtime and starts sending traces to TCC.
|
|
42
|
+
|
|
43
|
+
## Alternative: Manual Registration
|
|
44
|
+
|
|
45
|
+
If you prefer to register from a custom extension:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// extensions/tcc-observability/index.ts
|
|
49
|
+
import { register } from "@contextcompany/openclaw";
|
|
50
|
+
|
|
51
|
+
export default async function (api) {
|
|
52
|
+
register(api);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With explicit config:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
register(api, {
|
|
60
|
+
apiKey: "tcc_...",
|
|
61
|
+
endpoint: "https://api.thecontext.company/v1/openclaw",
|
|
62
|
+
debug: true,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Option | Env Var | Default | Description |
|
|
69
|
+
|--------|---------|---------|-------------|
|
|
70
|
+
| `apiKey` | `TCC_API_KEY` | — | Your Context Company API key |
|
|
71
|
+
| `endpoint` | `TCC_URL` | Auto-detected from key | Ingestion endpoint URL |
|
|
72
|
+
| `debug` | `TCC_DEBUG` | `false` | Enable debug logging |
|
|
73
|
+
|
|
74
|
+
## How It Works
|
|
75
|
+
|
|
76
|
+
The plugin hooks into OpenClaw's agent lifecycle events:
|
|
77
|
+
|
|
78
|
+
| Hook | What it captures |
|
|
79
|
+
|------|-----------------|
|
|
80
|
+
| `llm_input` | LLM call start — model, prompt, system prompt, history |
|
|
81
|
+
| `llm_output` | LLM call end — response, token usage, cost |
|
|
82
|
+
| `before_tool_call` | Tool execution start — tool name, arguments |
|
|
83
|
+
| `after_tool_call` | Tool execution end — result, errors, duration |
|
|
84
|
+
| `agent_end` | Run complete — success/failure, full message history |
|
|
85
|
+
|
|
86
|
+
All events are collected during the agent run and sent as a single batch when the run completes. Sessions that never receive an `agent_end` are flushed after 30 minutes.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
// src/transport.ts
|
|
6
|
+
var MAX_RETRIES = 2;
|
|
7
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
8
|
+
async function sendToTcc(payload, apiKey, url, debug, log) {
|
|
9
|
+
const body = JSON.stringify(payload);
|
|
10
|
+
if (debug) log.info(`sending ${body.length} bytes to ${url}`);
|
|
11
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
12
|
+
if (attempt > 0)
|
|
13
|
+
await new Promise((r) => setTimeout(r, INITIAL_BACKOFF_MS * 2 ** (attempt - 1)));
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${apiKey}`
|
|
20
|
+
},
|
|
21
|
+
body
|
|
22
|
+
});
|
|
23
|
+
if (res.ok) {
|
|
24
|
+
if (debug) log.info("sent ok");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const text = await res.text();
|
|
28
|
+
if (res.status !== 429 && res.status < 500) {
|
|
29
|
+
log.warn(`ingestion failed (${res.status}): ${text}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (attempt === MAX_RETRIES)
|
|
34
|
+
log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts: ${err}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts (server errors)`);
|
|
38
|
+
}
|
|
39
|
+
function safeClone(obj) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(JSON.stringify(obj));
|
|
42
|
+
} catch {
|
|
43
|
+
return String(obj);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/plugin.ts
|
|
48
|
+
function registerHooks(api, configOverrides) {
|
|
49
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
50
|
+
const pluginConfig = {
|
|
51
|
+
...api.pluginConfig ?? {},
|
|
52
|
+
...configOverrides
|
|
53
|
+
};
|
|
54
|
+
const debug = pluginConfig.debug === true || process.env.TCC_DEBUG === "true";
|
|
55
|
+
const log = {
|
|
56
|
+
info: (msg) => console.log(`[tcc] ${msg}`),
|
|
57
|
+
warn: (msg) => console.warn(`[tcc] ${msg}`)
|
|
58
|
+
};
|
|
59
|
+
const apiKey = (typeof pluginConfig.apiKey === "string" ? pluginConfig.apiKey : null) ?? process.env.TCC_API_KEY;
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
log.warn("No TCC_API_KEY found. Set env var or plugin config. Disabled.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const url = (typeof pluginConfig.endpoint === "string" ? pluginConfig.endpoint : null) ?? process.env.TCC_URL ?? (apiKey.startsWith("dev_") ? "https://dev.thecontext.company/v1/openclaw" : "https://api.thecontext.company/v1/openclaw");
|
|
65
|
+
log.info(`exporting runs to ${url}`);
|
|
66
|
+
const STALE_SESSION_MS = 30 * 60 * 1e3;
|
|
67
|
+
const cleanupInterval = setInterval(() => {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
for (const [key, session] of activeSessions) {
|
|
70
|
+
if (now - session.startedAt > STALE_SESSION_MS) {
|
|
71
|
+
if (debug) log.info(`flushing stale session: ${key}`);
|
|
72
|
+
sendToTcc(
|
|
73
|
+
{ framework: "openclaw", events: session.events, stale: true },
|
|
74
|
+
apiKey,
|
|
75
|
+
url,
|
|
76
|
+
debug,
|
|
77
|
+
log
|
|
78
|
+
).catch((err) => {
|
|
79
|
+
log.warn(`failed to send stale session: ${err}`);
|
|
80
|
+
});
|
|
81
|
+
activeSessions.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, 5 * 60 * 1e3);
|
|
85
|
+
if (cleanupInterval.unref) cleanupInterval.unref();
|
|
86
|
+
function pushEvent(hook, event, ctx) {
|
|
87
|
+
const sessionKey = ctx?.sessionKey;
|
|
88
|
+
if (!sessionKey) return;
|
|
89
|
+
let session = activeSessions.get(sessionKey);
|
|
90
|
+
if (!session) {
|
|
91
|
+
session = { events: [], startedAt: Date.now() };
|
|
92
|
+
activeSessions.set(sessionKey, session);
|
|
93
|
+
}
|
|
94
|
+
session.events.push({
|
|
95
|
+
hook,
|
|
96
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
event: safeClone(event),
|
|
98
|
+
context: safeClone(ctx)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
api.on("llm_input", (event, ctx) => {
|
|
102
|
+
pushEvent("llm_input", event, ctx);
|
|
103
|
+
if (debug) log.info(`llm_input (model: ${event.model})`);
|
|
104
|
+
});
|
|
105
|
+
api.on("llm_output", (event, ctx) => {
|
|
106
|
+
pushEvent("llm_output", event, ctx);
|
|
107
|
+
if (debug) log.info(`llm_output (model: ${event.model})`);
|
|
108
|
+
});
|
|
109
|
+
api.on("before_tool_call", (event, ctx) => {
|
|
110
|
+
pushEvent("before_tool_call", event, ctx);
|
|
111
|
+
if (debug) log.info(`before_tool_call (tool: ${event.toolName})`);
|
|
112
|
+
});
|
|
113
|
+
api.on("after_tool_call", (event, ctx) => {
|
|
114
|
+
pushEvent("after_tool_call", event, ctx);
|
|
115
|
+
if (debug) log.info(`after_tool_call (tool: ${event.toolName})`);
|
|
116
|
+
});
|
|
117
|
+
api.on("agent_end", (event, ctx) => {
|
|
118
|
+
const sessionKey = ctx?.sessionKey;
|
|
119
|
+
if (!sessionKey) return;
|
|
120
|
+
pushEvent("agent_end", event, ctx);
|
|
121
|
+
queueMicrotask(() => {
|
|
122
|
+
const session = activeSessions.get(sessionKey);
|
|
123
|
+
if (!session) return;
|
|
124
|
+
if (debug)
|
|
125
|
+
log.info(`agent_end \u2014 sending ${session.events.length} events`);
|
|
126
|
+
const payload = {
|
|
127
|
+
framework: "openclaw",
|
|
128
|
+
events: session.events
|
|
129
|
+
};
|
|
130
|
+
sendToTcc(payload, apiKey, url, debug, log).catch((err) => {
|
|
131
|
+
log.warn(`failed to send events: ${err}`);
|
|
132
|
+
});
|
|
133
|
+
activeSessions.delete(sessionKey);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
var plugin = {
|
|
138
|
+
id: "@contextcompany/openclaw",
|
|
139
|
+
name: "The Context Company",
|
|
140
|
+
description: "Agent observability \u2014 captures LLM calls, tool executions, and agent lifecycle events",
|
|
141
|
+
register(api) {
|
|
142
|
+
registerHooks(api);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var plugin_default = plugin;
|
|
146
|
+
function register(api, configOverrides) {
|
|
147
|
+
registerHooks(api, configOverrides);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
exports.default = plugin_default;
|
|
151
|
+
exports.register = register;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Configuration for the TCC OpenClaw plugin. */
|
|
2
|
+
type OpenClawPluginConfig = {
|
|
3
|
+
/** TCC API key. Falls back to TCC_API_KEY env var. */
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/** TCC ingestion endpoint. Falls back to TCC_URL env var, then auto-detected from key prefix. */
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
/** Enable debug logging. Falls back to TCC_DEBUG env var. */
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @contextcompany/openclaw — OpenClaw plugin for The Context Company
|
|
13
|
+
*
|
|
14
|
+
* Thin forwarder: collects raw hook events during an agent run,
|
|
15
|
+
* then sends them all as one batch on agent_end.
|
|
16
|
+
*
|
|
17
|
+
* All parsing/transformation happens server-side.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Full OpenClaw plugin object — install via `openclaw plugins install`
|
|
22
|
+
* and configure in `openclaw.json` under `plugins.entries`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```json
|
|
26
|
+
* {
|
|
27
|
+
* "plugins": {
|
|
28
|
+
* "allow": ["@contextcompany/openclaw"],
|
|
29
|
+
* "entries": {
|
|
30
|
+
* "@contextcompany/openclaw": {
|
|
31
|
+
* "enabled": true,
|
|
32
|
+
* "config": {
|
|
33
|
+
* "apiKey": "${TCC_API_KEY}"
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare const plugin: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
register(api: any): void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Named export for manual registration (e.g. from a custom extension).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { register } from "@contextcompany/openclaw";
|
|
54
|
+
* export default async function (api) {
|
|
55
|
+
* register(api, { apiKey: "tcc_...", debug: true });
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare function register(api: any, configOverrides?: OpenClawPluginConfig): void;
|
|
60
|
+
|
|
61
|
+
export { type OpenClawPluginConfig, plugin as default, register };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Configuration for the TCC OpenClaw plugin. */
|
|
2
|
+
type OpenClawPluginConfig = {
|
|
3
|
+
/** TCC API key. Falls back to TCC_API_KEY env var. */
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/** TCC ingestion endpoint. Falls back to TCC_URL env var, then auto-detected from key prefix. */
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
/** Enable debug logging. Falls back to TCC_DEBUG env var. */
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @contextcompany/openclaw — OpenClaw plugin for The Context Company
|
|
13
|
+
*
|
|
14
|
+
* Thin forwarder: collects raw hook events during an agent run,
|
|
15
|
+
* then sends them all as one batch on agent_end.
|
|
16
|
+
*
|
|
17
|
+
* All parsing/transformation happens server-side.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Full OpenClaw plugin object — install via `openclaw plugins install`
|
|
22
|
+
* and configure in `openclaw.json` under `plugins.entries`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```json
|
|
26
|
+
* {
|
|
27
|
+
* "plugins": {
|
|
28
|
+
* "allow": ["@contextcompany/openclaw"],
|
|
29
|
+
* "entries": {
|
|
30
|
+
* "@contextcompany/openclaw": {
|
|
31
|
+
* "enabled": true,
|
|
32
|
+
* "config": {
|
|
33
|
+
* "apiKey": "${TCC_API_KEY}"
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare const plugin: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
register(api: any): void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Named export for manual registration (e.g. from a custom extension).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { register } from "@contextcompany/openclaw";
|
|
54
|
+
* export default async function (api) {
|
|
55
|
+
* register(api, { apiKey: "tcc_...", debug: true });
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare function register(api: any, configOverrides?: OpenClawPluginConfig): void;
|
|
60
|
+
|
|
61
|
+
export { type OpenClawPluginConfig, plugin as default, register };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// src/transport.ts
|
|
2
|
+
var MAX_RETRIES = 2;
|
|
3
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
4
|
+
async function sendToTcc(payload, apiKey, url, debug, log) {
|
|
5
|
+
const body = JSON.stringify(payload);
|
|
6
|
+
if (debug) log.info(`sending ${body.length} bytes to ${url}`);
|
|
7
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
8
|
+
if (attempt > 0)
|
|
9
|
+
await new Promise((r) => setTimeout(r, INITIAL_BACKOFF_MS * 2 ** (attempt - 1)));
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
Authorization: `Bearer ${apiKey}`
|
|
16
|
+
},
|
|
17
|
+
body
|
|
18
|
+
});
|
|
19
|
+
if (res.ok) {
|
|
20
|
+
if (debug) log.info("sent ok");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
if (res.status !== 429 && res.status < 500) {
|
|
25
|
+
log.warn(`ingestion failed (${res.status}): ${text}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (attempt === MAX_RETRIES)
|
|
30
|
+
log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts: ${err}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts (server errors)`);
|
|
34
|
+
}
|
|
35
|
+
function safeClone(obj) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(JSON.stringify(obj));
|
|
38
|
+
} catch {
|
|
39
|
+
return String(obj);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/plugin.ts
|
|
44
|
+
function registerHooks(api, configOverrides) {
|
|
45
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
46
|
+
const pluginConfig = {
|
|
47
|
+
...api.pluginConfig ?? {},
|
|
48
|
+
...configOverrides
|
|
49
|
+
};
|
|
50
|
+
const debug = pluginConfig.debug === true || process.env.TCC_DEBUG === "true";
|
|
51
|
+
const log = {
|
|
52
|
+
info: (msg) => console.log(`[tcc] ${msg}`),
|
|
53
|
+
warn: (msg) => console.warn(`[tcc] ${msg}`)
|
|
54
|
+
};
|
|
55
|
+
const apiKey = (typeof pluginConfig.apiKey === "string" ? pluginConfig.apiKey : null) ?? process.env.TCC_API_KEY;
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
log.warn("No TCC_API_KEY found. Set env var or plugin config. Disabled.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const url = (typeof pluginConfig.endpoint === "string" ? pluginConfig.endpoint : null) ?? process.env.TCC_URL ?? (apiKey.startsWith("dev_") ? "https://dev.thecontext.company/v1/openclaw" : "https://api.thecontext.company/v1/openclaw");
|
|
61
|
+
log.info(`exporting runs to ${url}`);
|
|
62
|
+
const STALE_SESSION_MS = 30 * 60 * 1e3;
|
|
63
|
+
const cleanupInterval = setInterval(() => {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const [key, session] of activeSessions) {
|
|
66
|
+
if (now - session.startedAt > STALE_SESSION_MS) {
|
|
67
|
+
if (debug) log.info(`flushing stale session: ${key}`);
|
|
68
|
+
sendToTcc(
|
|
69
|
+
{ framework: "openclaw", events: session.events, stale: true },
|
|
70
|
+
apiKey,
|
|
71
|
+
url,
|
|
72
|
+
debug,
|
|
73
|
+
log
|
|
74
|
+
).catch((err) => {
|
|
75
|
+
log.warn(`failed to send stale session: ${err}`);
|
|
76
|
+
});
|
|
77
|
+
activeSessions.delete(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, 5 * 60 * 1e3);
|
|
81
|
+
if (cleanupInterval.unref) cleanupInterval.unref();
|
|
82
|
+
function pushEvent(hook, event, ctx) {
|
|
83
|
+
const sessionKey = ctx?.sessionKey;
|
|
84
|
+
if (!sessionKey) return;
|
|
85
|
+
let session = activeSessions.get(sessionKey);
|
|
86
|
+
if (!session) {
|
|
87
|
+
session = { events: [], startedAt: Date.now() };
|
|
88
|
+
activeSessions.set(sessionKey, session);
|
|
89
|
+
}
|
|
90
|
+
session.events.push({
|
|
91
|
+
hook,
|
|
92
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
93
|
+
event: safeClone(event),
|
|
94
|
+
context: safeClone(ctx)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
api.on("llm_input", (event, ctx) => {
|
|
98
|
+
pushEvent("llm_input", event, ctx);
|
|
99
|
+
if (debug) log.info(`llm_input (model: ${event.model})`);
|
|
100
|
+
});
|
|
101
|
+
api.on("llm_output", (event, ctx) => {
|
|
102
|
+
pushEvent("llm_output", event, ctx);
|
|
103
|
+
if (debug) log.info(`llm_output (model: ${event.model})`);
|
|
104
|
+
});
|
|
105
|
+
api.on("before_tool_call", (event, ctx) => {
|
|
106
|
+
pushEvent("before_tool_call", event, ctx);
|
|
107
|
+
if (debug) log.info(`before_tool_call (tool: ${event.toolName})`);
|
|
108
|
+
});
|
|
109
|
+
api.on("after_tool_call", (event, ctx) => {
|
|
110
|
+
pushEvent("after_tool_call", event, ctx);
|
|
111
|
+
if (debug) log.info(`after_tool_call (tool: ${event.toolName})`);
|
|
112
|
+
});
|
|
113
|
+
api.on("agent_end", (event, ctx) => {
|
|
114
|
+
const sessionKey = ctx?.sessionKey;
|
|
115
|
+
if (!sessionKey) return;
|
|
116
|
+
pushEvent("agent_end", event, ctx);
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
const session = activeSessions.get(sessionKey);
|
|
119
|
+
if (!session) return;
|
|
120
|
+
if (debug)
|
|
121
|
+
log.info(`agent_end \u2014 sending ${session.events.length} events`);
|
|
122
|
+
const payload = {
|
|
123
|
+
framework: "openclaw",
|
|
124
|
+
events: session.events
|
|
125
|
+
};
|
|
126
|
+
sendToTcc(payload, apiKey, url, debug, log).catch((err) => {
|
|
127
|
+
log.warn(`failed to send events: ${err}`);
|
|
128
|
+
});
|
|
129
|
+
activeSessions.delete(sessionKey);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
var plugin = {
|
|
134
|
+
id: "@contextcompany/openclaw",
|
|
135
|
+
name: "The Context Company",
|
|
136
|
+
description: "Agent observability \u2014 captures LLM calls, tool executions, and agent lifecycle events",
|
|
137
|
+
register(api) {
|
|
138
|
+
registerHooks(api);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var plugin_default = plugin;
|
|
142
|
+
function register(api, configOverrides) {
|
|
143
|
+
registerHooks(api, configOverrides);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { plugin_default as default, register };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "@contextcompany/openclaw",
|
|
3
|
+
"name": "The Context Company",
|
|
4
|
+
"description": "Agent observability — captures LLM calls, tool executions, and agent lifecycle events",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"enabled": { "type": "boolean" },
|
|
10
|
+
"apiKey": { "type": "string" },
|
|
11
|
+
"endpoint": { "type": "string" },
|
|
12
|
+
"debug": { "type": "boolean" }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"uiHints": {
|
|
16
|
+
"enabled": {
|
|
17
|
+
"label": "Tracing Enabled",
|
|
18
|
+
"help": "Enable trace export to The Context Company."
|
|
19
|
+
},
|
|
20
|
+
"apiKey": {
|
|
21
|
+
"label": "TCC API Key",
|
|
22
|
+
"placeholder": "tcc_...",
|
|
23
|
+
"sensitive": true,
|
|
24
|
+
"help": "Falls back to TCC_API_KEY environment variable."
|
|
25
|
+
},
|
|
26
|
+
"endpoint": {
|
|
27
|
+
"label": "TCC Endpoint URL",
|
|
28
|
+
"placeholder": "https://api.thecontext.company/v1/openclaw",
|
|
29
|
+
"help": "Auto-detected from API key prefix. Override for custom deployments."
|
|
30
|
+
},
|
|
31
|
+
"debug": {
|
|
32
|
+
"label": "Debug Logging",
|
|
33
|
+
"help": "Log hook events and transport details to console."
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contextcompany/openclaw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The Context Company integration for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"openclaw.plugin.json",
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"extension": "src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.cjs",
|
|
16
|
+
"module": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "pnpm build --watch"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"keywords": [
|
|
31
|
+
"openclaw",
|
|
32
|
+
"observability",
|
|
33
|
+
"tracing",
|
|
34
|
+
"monitoring",
|
|
35
|
+
"context-company",
|
|
36
|
+
"ai-agents"
|
|
37
|
+
],
|
|
38
|
+
"homepage": "https://github.com/The-Context-Company/observatory/tree/main/packages/ts/openclaw#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/The-Context-Company/observatory/issues"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/The-Context-Company/observatory",
|
|
45
|
+
"directory": "packages/ts/openclaw"
|
|
46
|
+
},
|
|
47
|
+
"author": "The Context Company",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20",
|
|
57
|
+
"tsup": "^8.5.0",
|
|
58
|
+
"typescript": "^5.9.3"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, register, type OpenClawPluginConfig } from "./plugin.js";
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @contextcompany/openclaw — OpenClaw plugin for The Context Company
|
|
3
|
+
*
|
|
4
|
+
* Thin forwarder: collects raw hook events during an agent run,
|
|
5
|
+
* then sends them all as one batch on agent_end.
|
|
6
|
+
*
|
|
7
|
+
* All parsing/transformation happens server-side.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ActiveSession, OpenClawPluginConfig } from "./types.js";
|
|
11
|
+
export type { OpenClawPluginConfig } from "./types.js";
|
|
12
|
+
import { safeClone, sendToTcc } from "./transport.js";
|
|
13
|
+
|
|
14
|
+
function registerHooks(
|
|
15
|
+
api: any,
|
|
16
|
+
configOverrides?: OpenClawPluginConfig,
|
|
17
|
+
): void {
|
|
18
|
+
const activeSessions = new Map<string, ActiveSession>();
|
|
19
|
+
|
|
20
|
+
const pluginConfig: Record<string, unknown> = {
|
|
21
|
+
...(api.pluginConfig ?? {}),
|
|
22
|
+
...configOverrides,
|
|
23
|
+
};
|
|
24
|
+
const debug =
|
|
25
|
+
pluginConfig.debug === true || process.env.TCC_DEBUG === "true";
|
|
26
|
+
|
|
27
|
+
const log = {
|
|
28
|
+
info: (msg: string) => console.log(`[tcc] ${msg}`),
|
|
29
|
+
warn: (msg: string) => console.warn(`[tcc] ${msg}`),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const apiKey =
|
|
33
|
+
(typeof pluginConfig.apiKey === "string" ? pluginConfig.apiKey : null) ??
|
|
34
|
+
process.env.TCC_API_KEY;
|
|
35
|
+
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
log.warn("No TCC_API_KEY found. Set env var or plugin config. Disabled.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url =
|
|
42
|
+
(typeof pluginConfig.endpoint === "string"
|
|
43
|
+
? pluginConfig.endpoint
|
|
44
|
+
: null) ??
|
|
45
|
+
process.env.TCC_URL ??
|
|
46
|
+
(apiKey.startsWith("dev_")
|
|
47
|
+
? "https://dev.thecontext.company/v1/openclaw"
|
|
48
|
+
: "https://api.thecontext.company/v1/openclaw");
|
|
49
|
+
|
|
50
|
+
log.info(`exporting runs to ${url}`);
|
|
51
|
+
|
|
52
|
+
// -------------------------------------------------------------------
|
|
53
|
+
// Stale session cleanup — flush sessions that never got an agent_end
|
|
54
|
+
// -------------------------------------------------------------------
|
|
55
|
+
const STALE_SESSION_MS = 30 * 60 * 1000; // 30 minutes
|
|
56
|
+
|
|
57
|
+
const cleanupInterval = setInterval(() => {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const [key, session] of activeSessions) {
|
|
60
|
+
if (now - session.startedAt > STALE_SESSION_MS) {
|
|
61
|
+
if (debug) log.info(`flushing stale session: ${key}`);
|
|
62
|
+
|
|
63
|
+
sendToTcc(
|
|
64
|
+
{ framework: "openclaw", events: session.events, stale: true },
|
|
65
|
+
apiKey,
|
|
66
|
+
url,
|
|
67
|
+
debug,
|
|
68
|
+
log,
|
|
69
|
+
).catch((err) => {
|
|
70
|
+
log.warn(`failed to send stale session: ${err}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
activeSessions.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, 5 * 60 * 1000); // check every 5 minutes
|
|
77
|
+
|
|
78
|
+
if (cleanupInterval.unref) cleanupInterval.unref();
|
|
79
|
+
|
|
80
|
+
// -------------------------------------------------------------------
|
|
81
|
+
// Helper: push a raw event into the session's event list
|
|
82
|
+
// -------------------------------------------------------------------
|
|
83
|
+
function pushEvent(hook: string, event: unknown, ctx: unknown): void {
|
|
84
|
+
const sessionKey = (ctx as any)?.sessionKey;
|
|
85
|
+
if (!sessionKey) return;
|
|
86
|
+
|
|
87
|
+
let session = activeSessions.get(sessionKey);
|
|
88
|
+
if (!session) {
|
|
89
|
+
session = { events: [], startedAt: Date.now() };
|
|
90
|
+
activeSessions.set(sessionKey, session);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
session.events.push({
|
|
94
|
+
hook,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
event: safeClone(event) as Record<string, unknown>,
|
|
97
|
+
context: safeClone(ctx) as Record<string, unknown>,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// -------------------------------------------------------------------
|
|
102
|
+
// Hooks: collect raw events
|
|
103
|
+
// -------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
api.on("llm_input", (event: any, ctx: any) => {
|
|
106
|
+
pushEvent("llm_input", event, ctx);
|
|
107
|
+
if (debug) log.info(`llm_input (model: ${event.model})`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
api.on("llm_output", (event: any, ctx: any) => {
|
|
111
|
+
pushEvent("llm_output", event, ctx);
|
|
112
|
+
if (debug) log.info(`llm_output (model: ${event.model})`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
api.on("before_tool_call", (event: any, ctx: any) => {
|
|
116
|
+
pushEvent("before_tool_call", event, ctx);
|
|
117
|
+
if (debug) log.info(`before_tool_call (tool: ${event.toolName})`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
api.on("after_tool_call", (event: any, ctx: any) => {
|
|
121
|
+
pushEvent("after_tool_call", event, ctx);
|
|
122
|
+
if (debug) log.info(`after_tool_call (tool: ${event.toolName})`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
api.on("agent_end", (event: any, ctx: any) => {
|
|
126
|
+
const sessionKey = ctx?.sessionKey;
|
|
127
|
+
if (!sessionKey) return;
|
|
128
|
+
|
|
129
|
+
pushEvent("agent_end", event, ctx);
|
|
130
|
+
|
|
131
|
+
// Defer sending to a microtask so llm_output (which fires on the
|
|
132
|
+
// same synchronous tick as agent_end) gets collected first.
|
|
133
|
+
queueMicrotask(() => {
|
|
134
|
+
const session = activeSessions.get(sessionKey);
|
|
135
|
+
if (!session) return;
|
|
136
|
+
|
|
137
|
+
if (debug)
|
|
138
|
+
log.info(`agent_end — sending ${session.events.length} events`);
|
|
139
|
+
|
|
140
|
+
const payload = {
|
|
141
|
+
framework: "openclaw",
|
|
142
|
+
events: session.events,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
sendToTcc(payload, apiKey, url, debug, log).catch((err) => {
|
|
146
|
+
log.warn(`failed to send events: ${err}`);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
activeSessions.delete(sessionKey);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Full OpenClaw plugin object — install via `openclaw plugins install`
|
|
156
|
+
* and configure in `openclaw.json` under `plugins.entries`.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```json
|
|
160
|
+
* {
|
|
161
|
+
* "plugins": {
|
|
162
|
+
* "allow": ["@contextcompany/openclaw"],
|
|
163
|
+
* "entries": {
|
|
164
|
+
* "@contextcompany/openclaw": {
|
|
165
|
+
* "enabled": true,
|
|
166
|
+
* "config": {
|
|
167
|
+
* "apiKey": "${TCC_API_KEY}"
|
|
168
|
+
* }
|
|
169
|
+
* }
|
|
170
|
+
* }
|
|
171
|
+
* }
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
const plugin = {
|
|
176
|
+
id: "@contextcompany/openclaw",
|
|
177
|
+
name: "The Context Company",
|
|
178
|
+
description: "Agent observability — captures LLM calls, tool executions, and agent lifecycle events",
|
|
179
|
+
register(api: any) {
|
|
180
|
+
registerHooks(api);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default plugin;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Named export for manual registration (e.g. from a custom extension).
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* import { register } from "@contextcompany/openclaw";
|
|
192
|
+
* export default async function (api) {
|
|
193
|
+
* register(api, { apiKey: "tcc_...", debug: true });
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function register(
|
|
198
|
+
api: any,
|
|
199
|
+
configOverrides?: OpenClawPluginConfig,
|
|
200
|
+
): void {
|
|
201
|
+
registerHooks(api, configOverrides);
|
|
202
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const MAX_RETRIES = 2;
|
|
2
|
+
const INITIAL_BACKOFF_MS = 1000;
|
|
3
|
+
|
|
4
|
+
export async function sendToTcc(
|
|
5
|
+
payload: Record<string, unknown>,
|
|
6
|
+
apiKey: string,
|
|
7
|
+
url: string,
|
|
8
|
+
debug: boolean,
|
|
9
|
+
log: { info: (m: string) => void; warn: (m: string) => void },
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const body = JSON.stringify(payload);
|
|
12
|
+
if (debug) log.info(`sending ${body.length} bytes to ${url}`);
|
|
13
|
+
|
|
14
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
15
|
+
if (attempt > 0)
|
|
16
|
+
await new Promise((r) => setTimeout(r, INITIAL_BACKOFF_MS * 2 ** (attempt - 1)));
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
Authorization: `Bearer ${apiKey}`,
|
|
23
|
+
},
|
|
24
|
+
body,
|
|
25
|
+
});
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
if (debug) log.info("sent ok");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
if (res.status !== 429 && res.status < 500) {
|
|
32
|
+
log.warn(`ingestion failed (${res.status}): ${text}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (attempt === MAX_RETRIES)
|
|
37
|
+
log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts: ${err}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
log.warn(`ingestion failed after ${MAX_RETRIES + 1} attempts (server errors)`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function safeClone(obj: unknown): unknown {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(JSON.stringify(obj));
|
|
46
|
+
} catch {
|
|
47
|
+
return String(obj);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A single raw hook event captured from OpenClaw's plugin API. */
|
|
2
|
+
export type RawEvent = {
|
|
3
|
+
hook: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
event: Record<string, unknown>;
|
|
6
|
+
context: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** In-flight session accumulating events before send. */
|
|
10
|
+
export type ActiveSession = {
|
|
11
|
+
events: RawEvent[];
|
|
12
|
+
startedAt: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Configuration for the TCC OpenClaw plugin. */
|
|
16
|
+
export type OpenClawPluginConfig = {
|
|
17
|
+
/** TCC API key. Falls back to TCC_API_KEY env var. */
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** TCC ingestion endpoint. Falls back to TCC_URL env var, then auto-detected from key prefix. */
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
/** Enable debug logging. Falls back to TCC_DEBUG env var. */
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
};
|