@turingpulse/sdk 1.0.1

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.
Files changed (160) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/ci.yml +246 -0
  3. package/.github/workflows/framework-compat.yml +169 -0
  4. package/.github/workflows/security.yml +336 -0
  5. package/CHANGELOG.md +29 -0
  6. package/LICENSE +13 -0
  7. package/MIGRATION.md +30 -0
  8. package/README.md +221 -0
  9. package/dist/attachments.d.ts +28 -0
  10. package/dist/attachments.d.ts.map +1 -0
  11. package/dist/attachments.js +59 -0
  12. package/dist/attachments.js.map +1 -0
  13. package/dist/config.d.ts +72 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +78 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +126 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +163 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/decorators.d.ts +6 -0
  22. package/dist/decorators.d.ts.map +1 -0
  23. package/dist/decorators.js +52 -0
  24. package/dist/decorators.js.map +1 -0
  25. package/dist/deploy.d.ts +89 -0
  26. package/dist/deploy.d.ts.map +1 -0
  27. package/dist/deploy.js +203 -0
  28. package/dist/deploy.js.map +1 -0
  29. package/dist/errors.d.ts +18 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +34 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/eventBuilder.d.ts +21 -0
  34. package/dist/eventBuilder.d.ts.map +1 -0
  35. package/dist/eventBuilder.js +127 -0
  36. package/dist/eventBuilder.js.map +1 -0
  37. package/dist/fingerprint.d.ts +158 -0
  38. package/dist/fingerprint.d.ts.map +1 -0
  39. package/dist/fingerprint.js +339 -0
  40. package/dist/fingerprint.js.map +1 -0
  41. package/dist/governance.d.ts +47 -0
  42. package/dist/governance.d.ts.map +1 -0
  43. package/dist/governance.js +104 -0
  44. package/dist/governance.js.map +1 -0
  45. package/dist/http.d.ts +62 -0
  46. package/dist/http.d.ts.map +1 -0
  47. package/dist/http.js +181 -0
  48. package/dist/http.js.map +1 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +23 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation.d.ts +40 -0
  54. package/dist/instrumentation.d.ts.map +1 -0
  55. package/dist/instrumentation.js +31 -0
  56. package/dist/instrumentation.js.map +1 -0
  57. package/dist/integrations/mastra.d.ts +64 -0
  58. package/dist/integrations/mastra.d.ts.map +1 -0
  59. package/dist/integrations/mastra.js +256 -0
  60. package/dist/integrations/mastra.js.map +1 -0
  61. package/dist/kpi.d.ts +21 -0
  62. package/dist/kpi.d.ts.map +1 -0
  63. package/dist/kpi.js +83 -0
  64. package/dist/kpi.js.map +1 -0
  65. package/dist/llmDetector.d.ts +22 -0
  66. package/dist/llmDetector.d.ts.map +1 -0
  67. package/dist/llmDetector.js +269 -0
  68. package/dist/llmDetector.js.map +1 -0
  69. package/dist/plugin.d.ts +33 -0
  70. package/dist/plugin.d.ts.map +1 -0
  71. package/dist/plugin.js +312 -0
  72. package/dist/plugin.js.map +1 -0
  73. package/dist/registry.d.ts +13 -0
  74. package/dist/registry.d.ts.map +1 -0
  75. package/dist/registry.js +18 -0
  76. package/dist/registry.js.map +1 -0
  77. package/dist/tracing.d.ts +10 -0
  78. package/dist/tracing.d.ts.map +1 -0
  79. package/dist/tracing.js +30 -0
  80. package/dist/tracing.js.map +1 -0
  81. package/dist/triggerState.d.ts +5 -0
  82. package/dist/triggerState.d.ts.map +1 -0
  83. package/dist/triggerState.js +19 -0
  84. package/dist/triggerState.js.map +1 -0
  85. package/dist/utils.d.ts +27 -0
  86. package/dist/utils.d.ts.map +1 -0
  87. package/dist/utils.js +72 -0
  88. package/dist/utils.js.map +1 -0
  89. package/package.json +37 -0
  90. package/packages/anthropic/package.json +16 -0
  91. package/packages/anthropic/src/index.ts +5 -0
  92. package/packages/anthropic/src/wrapper.ts +102 -0
  93. package/packages/anthropic/tsconfig.build.json +20 -0
  94. package/packages/langchain/package.json +16 -0
  95. package/packages/langchain/src/index.ts +7 -0
  96. package/packages/langchain/src/wrapper.ts +51 -0
  97. package/packages/mastra/package.json +17 -0
  98. package/packages/mastra/src/index.ts +8 -0
  99. package/packages/mastra/src/wrapper.ts +301 -0
  100. package/packages/openai/package.json +16 -0
  101. package/packages/openai/src/index.ts +8 -0
  102. package/packages/openai/src/wrapper.ts +103 -0
  103. package/packages/openai/tsconfig.build.json +20 -0
  104. package/packages/openclaw/openclaw.plugin.json +100 -0
  105. package/packages/openclaw/package.json +41 -0
  106. package/packages/openclaw/src/buffer.ts +99 -0
  107. package/packages/openclaw/src/config.ts +139 -0
  108. package/packages/openclaw/src/hooks/governance.ts +267 -0
  109. package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
  110. package/packages/openclaw/src/hooks/telemetry.ts +207 -0
  111. package/packages/openclaw/src/index.ts +91 -0
  112. package/packages/openclaw/src/mapper.ts +233 -0
  113. package/packages/openclaw/src/session-tracker.ts +181 -0
  114. package/packages/openclaw/src/types.ts +220 -0
  115. package/packages/openclaw/tests/buffer.test.ts +148 -0
  116. package/packages/openclaw/tests/config.test.ts +122 -0
  117. package/packages/openclaw/tests/governance.test.ts +232 -0
  118. package/packages/openclaw/tests/mapper.test.ts +242 -0
  119. package/packages/openclaw/tests/session-tracker.test.ts +124 -0
  120. package/packages/openclaw/tsconfig.json +18 -0
  121. package/packages/openclaw/vitest.config.ts +8 -0
  122. package/packages/vercel-ai/package.json +16 -0
  123. package/packages/vercel-ai/src/index.ts +5 -0
  124. package/packages/vercel-ai/src/wrapper.ts +49 -0
  125. package/scripts/bump-version.sh +58 -0
  126. package/scripts/update-readme-compat.mjs +151 -0
  127. package/src/__tests__/fingerprint.test.ts +328 -0
  128. package/src/attachments.ts +88 -0
  129. package/src/config.ts +164 -0
  130. package/src/context.ts +258 -0
  131. package/src/decorators.ts +61 -0
  132. package/src/deploy.ts +260 -0
  133. package/src/errors.ts +44 -0
  134. package/src/eventBuilder.ts +153 -0
  135. package/src/fingerprint.ts +421 -0
  136. package/src/governance.ts +156 -0
  137. package/src/http.ts +241 -0
  138. package/src/index.ts +57 -0
  139. package/src/instrumentation.ts +68 -0
  140. package/src/integrations/mastra.ts +335 -0
  141. package/src/kpi.ts +112 -0
  142. package/src/llmDetector.ts +330 -0
  143. package/src/plugin.ts +384 -0
  144. package/src/registry.ts +27 -0
  145. package/src/tracing.ts +39 -0
  146. package/src/triggerState.ts +27 -0
  147. package/src/utils.ts +78 -0
  148. package/tests/compat/anthropic.test.ts +61 -0
  149. package/tests/compat/cohere.test.ts +57 -0
  150. package/tests/compat/google-genai.test.ts +61 -0
  151. package/tests/compat/langchain-openai.test.ts +41 -0
  152. package/tests/compat/langchain.test.ts +64 -0
  153. package/tests/compat/mistral.test.ts +58 -0
  154. package/tests/compat/openai.test.ts +71 -0
  155. package/tests/compat/vercel-ai.test.ts +56 -0
  156. package/tests/plugins/anthropic-wrapper.test.ts +120 -0
  157. package/tests/plugins/langchain-wrapper.test.ts +128 -0
  158. package/tests/plugins/openai-wrapper.test.ts +165 -0
  159. package/tsconfig.json +21 -0
  160. package/vitest.config.ts +9 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Mastra 1.2.x instrumentation for TuringPulse SDK (TypeScript).
3
+ *
4
+ * Wraps `agent.generate()` to capture:
5
+ * - Token counts from `response.usage` (ai-sdk format: promptTokens / completionTokens).
6
+ * - Per-tool-call child spans.
7
+ * - Agent instructions as system prompt.
8
+ *
9
+ * This integration uses the centralized TuringPulseHttpClient when a plugin
10
+ * instance is available (preferred), or falls back to standalone HTTP when
11
+ * used without the full SDK.
12
+ */
13
+
14
+ import type {
15
+ AgentEvent,
16
+ } from '@turingpulse/interfaces';
17
+
18
+ import type { ToolCallRecord } from '@turingpulse/sdk';
19
+ import { generateUUID } from '@turingpulse/sdk';
20
+ import { DEFAULT_ENDPOINT } from '@turingpulse/sdk';
21
+ import type { TuringPulseHttpClient } from '@turingpulse/sdk';
22
+ import { SDK_VERSION } from '@turingpulse/sdk';
23
+ import { safeEnv, safeErrorMessage, toSnakeCaseDeep } from '@turingpulse/sdk';
24
+
25
+ const FRAMEWORK_NAME = 'mastra';
26
+ const FRAMEWORK_VERSION = '1.2.0';
27
+
28
+ /** Mastra tool call with required result/id fields. */
29
+ interface MastraToolCallRecord extends ToolCallRecord {
30
+ toolResult: string;
31
+ toolId: string;
32
+ }
33
+
34
+ interface MastraInstrumentOptions {
35
+ /** Workflow display name for TuringPulse. */
36
+ name: string;
37
+ /** LLM model name (default: gpt-4o-mini). */
38
+ model?: string;
39
+ /** LLM provider (default: openai). */
40
+ provider?: string;
41
+ /**
42
+ * Preferred: pass the plugin's HTTP client for centralized retries, timeouts, headers.
43
+ * If not provided, falls back to standalone fetch using apiKey + endpoint.
44
+ */
45
+ httpClient?: TuringPulseHttpClient;
46
+ /** TuringPulse API endpoint — only needed if httpClient is not provided. */
47
+ endpoint?: string;
48
+ /** TuringPulse API key — only needed if httpClient is not provided. */
49
+ apiKey?: string;
50
+ /** Tool names available to the agent. */
51
+ toolNames?: string[];
52
+ /**
53
+ * A reference to the mutable tool-call log array. Tool execute()
54
+ * callbacks push records here during agent.generate(). The integration
55
+ * reads from this array after execution completes.
56
+ */
57
+ toolCallLog?: MastraToolCallRecord[];
58
+ }
59
+
60
+ /**
61
+ * Instrument a Mastra Agent for TuringPulse observability.
62
+ *
63
+ * Returns an async function that runs `agent.generate()` and emits
64
+ * a root workflow span plus per-tool/LLM child spans.
65
+ */
66
+ export function instrumentMastra(
67
+ agent: { generate: (prompt: string) => Promise<unknown>; [key: string]: unknown },
68
+ options: MastraInstrumentOptions,
69
+ ): (prompt: string) => Promise<unknown> {
70
+ const {
71
+ name,
72
+ model = 'gpt-4o-mini',
73
+ provider = 'openai',
74
+ httpClient,
75
+ endpoint = safeEnv('TP_ENDPOINT') || DEFAULT_ENDPOINT,
76
+ apiKey = (safeEnv('TP_API_KEY') || '').trim(),
77
+ toolNames = [],
78
+ toolCallLog: externalLog,
79
+ } = options;
80
+
81
+ if (!httpClient && !apiKey) {
82
+ // eslint-disable-next-line no-console
83
+ console.warn(
84
+ 'TuringPulse: No httpClient or apiKey provided. Telemetry will be skipped. ' +
85
+ 'Pass the plugin\'s client via httpClient for best results.',
86
+ );
87
+ }
88
+
89
+ const systemPrompt: string =
90
+ typeof (agent as Record<string, unknown>).instructions === 'string'
91
+ ? ((agent as Record<string, unknown>).instructions as string).slice(0, 500)
92
+ : '';
93
+
94
+ return async function runInstrumented(prompt: string): Promise<unknown> {
95
+ const traceId = generateUUID();
96
+ const rootSpanId = generateUUID();
97
+ const t0 = Date.now();
98
+
99
+ if (externalLog) externalLog.length = 0;
100
+
101
+ const maxFieldLen = httpClient?.maxSerializedFieldLength ?? 50_000;
102
+
103
+ let response: unknown;
104
+ let generationError: Error | null = null;
105
+ try {
106
+ response = await agent.generate(prompt);
107
+ } catch (err) {
108
+ generationError = err instanceof Error ? err : new Error(String(err));
109
+ response = null;
110
+ }
111
+ const totalDurationMs = Date.now() - t0;
112
+
113
+ const resp = (response as Record<string, unknown>) || {};
114
+ const finalOutput = (resp.text as string) || '';
115
+ const usage = (resp.usage as { promptTokens?: number; completionTokens?: number }) || {};
116
+ const promptTokens = usage.promptTokens || 0;
117
+ const completionTokens = usage.completionTokens || 0;
118
+
119
+ const estPrompt = promptTokens || Math.max(Math.round(prompt.length / 4), 400);
120
+ const estCompletion = completionTokens || Math.max(Math.round(finalOutput.length / 4), 200);
121
+
122
+ const tcLog = externalLog ? [...externalLog] : [];
123
+
124
+ // ── Build child spans ──
125
+ const childEvents: AgentEvent[] = [];
126
+ const now = new Date();
127
+ let offset = 100;
128
+
129
+ for (const tc of tcLog) {
130
+ const spanId = generateUUID();
131
+ const ts = new Date(now.getTime() + offset).toISOString();
132
+ childEvents.push({
133
+ runId: traceId,
134
+ agentId: `tool_${tc.toolName}`,
135
+ timestamp: ts,
136
+ type: 'span',
137
+ payload: {
138
+ name: `${name}.tool_${tc.toolName}`,
139
+ durationMs: Math.floor(totalDurationMs / (tcLog.length + 1)),
140
+ status: tc.success ? 'success' : 'error',
141
+ metadata: {
142
+ span_id: spanId,
143
+ parent_span_id: rootSpanId,
144
+ trace_id: traceId,
145
+ workflow_name: name,
146
+ framework: FRAMEWORK_NAME,
147
+ node_type: 'tool',
148
+ depth: '1',
149
+ input: JSON.stringify(tc.toolArgs).slice(0, maxFieldLen),
150
+ output: JSON.stringify({ result: tc.toolResult.slice(0, maxFieldLen) }),
151
+ available_tools: JSON.stringify(toolNames),
152
+ tool_name: tc.toolName,
153
+ ...(tc.errorMessage ? { error_message: tc.errorMessage } : {}),
154
+ mastra_tool_id: tc.toolName,
155
+ mastra_version: FRAMEWORK_VERSION,
156
+ },
157
+ toolCalls: [{
158
+ toolName: tc.toolName,
159
+ toolArgs: tc.toolArgs,
160
+ toolResult: tc.toolResult.slice(0, maxFieldLen),
161
+ toolId: tc.toolId,
162
+ success: tc.success,
163
+ errorMessage: tc.errorMessage,
164
+ }],
165
+ },
166
+ });
167
+ offset += Math.floor(totalDurationMs / (tcLog.length + 1)) + 50;
168
+ }
169
+
170
+ // LLM reasoning span
171
+ const llmSpanId = generateUUID();
172
+ childEvents.push({
173
+ runId: traceId,
174
+ agentId: 'agent_reasoning',
175
+ timestamp: new Date(now.getTime() + offset).toISOString(),
176
+ type: 'span',
177
+ payload: {
178
+ name: `${name}.agent_reasoning`,
179
+ durationMs: Math.floor(totalDurationMs / (tcLog.length + 1)),
180
+ status: 'success',
181
+ tokens: { prompt: Math.round(estPrompt), completion: Math.round(estCompletion) },
182
+ costUsd: 0,
183
+ metadata: {
184
+ span_id: llmSpanId,
185
+ parent_span_id: rootSpanId,
186
+ trace_id: traceId,
187
+ workflow_name: name,
188
+ framework: FRAMEWORK_NAME,
189
+ node_type: 'llm',
190
+ depth: '1',
191
+ prompt: prompt.slice(0, maxFieldLen),
192
+ user_query: prompt.slice(0, maxFieldLen),
193
+ system_prompt: systemPrompt,
194
+ model,
195
+ provider,
196
+ available_tools: JSON.stringify(toolNames),
197
+ mastra_agent_name: (agent as Record<string, unknown>).name as string || name,
198
+ mastra_model: model,
199
+ mastra_tool_count: String(tcLog.length),
200
+ mastra_version: FRAMEWORK_VERSION,
201
+ },
202
+ },
203
+ });
204
+
205
+ // ── Root span ──
206
+ const totalTokensIn = childEvents.reduce((s, e) => s + (e.payload.tokens?.prompt || 0), 0);
207
+ const totalTokensOut = childEvents.reduce((s, e) => s + (e.payload.tokens?.completion || 0), 0);
208
+
209
+ const rootEvent: AgentEvent = {
210
+ runId: traceId,
211
+ agentId: name,
212
+ timestamp: now.toISOString(),
213
+ type: 'span',
214
+ payload: {
215
+ name: `${name}.execute`,
216
+ durationMs: totalDurationMs,
217
+ status: generationError ? 'error' : 'success',
218
+ error: generationError?.message?.slice(0, 500),
219
+ tokens: {
220
+ prompt: totalTokensIn || Math.round(estPrompt),
221
+ completion: totalTokensOut || Math.round(estCompletion),
222
+ },
223
+ costUsd: 0,
224
+ metadata: {
225
+ workflow_name: name,
226
+ framework: FRAMEWORK_NAME,
227
+ environment: safeEnv('NODE_ENV') || 'unknown',
228
+ input: prompt.slice(0, maxFieldLen),
229
+ output: finalOutput.slice(0, maxFieldLen),
230
+ span_id: rootSpanId,
231
+ trace_id: traceId,
232
+ node_type: 'workflow',
233
+ mastra_version: FRAMEWORK_VERSION,
234
+ ...(generationError ? { error_message: generationError.message.slice(0, 500) } : {}),
235
+ },
236
+ },
237
+ };
238
+
239
+ // ── Send all events via centralized client or standalone ──
240
+ const allEvents = [rootEvent, ...childEvents];
241
+
242
+ if (httpClient) {
243
+ await httpClient.emitEvents(allEvents);
244
+ } else if (apiKey && endpoint) {
245
+ await sendEventsStandalone(allEvents, endpoint, apiKey);
246
+ }
247
+
248
+ if (generationError) throw generationError;
249
+ return response;
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Standalone fallback when no httpClient is available.
255
+ * Prefer passing httpClient for retries, timeouts, and consistent headers.
256
+ */
257
+ const STANDALONE_TIMEOUT_MS = 10_000;
258
+ const STANDALONE_MAX_PAYLOAD_BYTES = 5 * 1024 * 1024;
259
+
260
+ async function sendEventsStandalone(
261
+ events: AgentEvent[],
262
+ baseUrl: string,
263
+ apiKey: string,
264
+ ): Promise<void> {
265
+ const url = `${baseUrl.replace(/\/$/, '')}/api/v1/sdk/events`;
266
+ const snakeEvents = toSnakeCaseDeep(events);
267
+ let body = JSON.stringify({ events: snakeEvents });
268
+ if (body.length > STANDALONE_MAX_PAYLOAD_BYTES && Array.isArray(snakeEvents) && (snakeEvents as unknown[]).length > 1) {
269
+ const arr = snakeEvents as unknown[];
270
+ const overhead = body.length - JSON.stringify(arr).length;
271
+ const avg = Math.ceil((body.length - overhead) / arr.length);
272
+ const target = Math.max(1, Math.floor((STANDALONE_MAX_PAYLOAD_BYTES - overhead) / avg));
273
+ body = JSON.stringify({ events: arr.slice(0, target) });
274
+ }
275
+ const controller = new AbortController();
276
+ const timeout = setTimeout(() => controller.abort(), STANDALONE_TIMEOUT_MS);
277
+ try {
278
+ const resp = await fetch(url, {
279
+ method: 'POST',
280
+ headers: {
281
+ 'Content-Type': 'application/json',
282
+ 'X-API-Key': apiKey,
283
+ 'X-SDK-Version': SDK_VERSION,
284
+ 'X-SDK-Language': 'typescript',
285
+ },
286
+ body,
287
+ signal: controller.signal as AbortSignal,
288
+ });
289
+ if (!resp.ok) {
290
+ // eslint-disable-next-line no-console
291
+ console.warn(`TuringPulse telemetry FAILED: ${resp.status}`);
292
+ }
293
+ } catch (e) {
294
+ // eslint-disable-next-line no-console
295
+ console.warn(`TuringPulse telemetry ERROR: ${safeErrorMessage(e)}`);
296
+ } finally {
297
+ clearTimeout(timeout);
298
+ }
299
+ }
300
+
301
+ export type { MastraInstrumentOptions, MastraToolCallRecord };
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@turingpulse/sdk-openai",
3
+ "version": "1.0.1",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "TuringPulse SDK integration for OpenAI (TypeScript)",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "dependencies": {
10
+ "@turingpulse/sdk": ">=1.0.0"
11
+ },
12
+ "peerDependencies": {
13
+ "openai": ">=4.0.0",
14
+ "typescript": ">=5.0.0"
15
+ }
16
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * TuringPulse SDK integration for OpenAI (TypeScript).
3
+ *
4
+ * Provides patch/unpatch and Proxy patterns for intercepting
5
+ * OpenAI chat completion calls.
6
+ */
7
+
8
+ export { patchOpenAI, unpatchOpenAI } from './wrapper';
@@ -0,0 +1,103 @@
1
+ import { withInstrumentation, GovernanceDirective, ConfigurationError, currentContext } from '@turingpulse/sdk';
2
+
3
+ let _originalCreate: unknown = null;
4
+ let _patchedProto: Record<string, unknown> | null = null;
5
+
6
+ interface PatchOpenAIOptions {
7
+ name?: string;
8
+ governance?: GovernanceDirective;
9
+ /**
10
+ * Pass your own imported OpenAI module to avoid module resolution mismatches.
11
+ * Accepts the default export class from `import OpenAI from 'openai'`
12
+ * or the full module namespace from `import * as mod from 'openai'`.
13
+ */
14
+ openaiModule?: unknown;
15
+ }
16
+
17
+ function resolveCompletionsProto(mod: unknown): Record<string, unknown> | undefined {
18
+ const cls = (mod as Record<string, unknown>)?.default ?? mod;
19
+ const chat = (cls as Record<string, unknown>)?.Chat as Record<string, unknown> | undefined;
20
+ const completions = chat?.Completions as Record<string, unknown> | undefined;
21
+ return completions?.prototype as Record<string, unknown> | undefined;
22
+ }
23
+
24
+ /**
25
+ * Monkey-patch OpenAI chat completions for TuringPulse instrumentation.
26
+ */
27
+ export async function patchOpenAI(options: PatchOpenAIOptions = {}): Promise<void> {
28
+ const effectiveName = options.name || (typeof process !== 'undefined' ? process.env?.TP_WORKFLOW_NAME : undefined) || '';
29
+ if (!effectiveName) {
30
+ throw new ConfigurationError('patchOpenAI() requires name or TP_WORKFLOW_NAME env var.');
31
+ }
32
+
33
+ let completionsProto: Record<string, unknown> | undefined;
34
+
35
+ if (options.openaiModule) {
36
+ completionsProto = resolveCompletionsProto(options.openaiModule);
37
+ } else {
38
+ try {
39
+ const mod = await import('openai');
40
+ completionsProto = resolveCompletionsProto(mod);
41
+ } catch {
42
+ throw new Error('openai package is required: npm install openai');
43
+ }
44
+ }
45
+
46
+ if (!completionsProto) {
47
+ console.warn('Cannot locate OpenAI.Chat.Completions.prototype — skipping patch');
48
+ return;
49
+ }
50
+
51
+ if (_originalCreate !== null) {
52
+ console.warn('OpenAI is already patched — skipping duplicate patchOpenAI()');
53
+ return;
54
+ }
55
+
56
+ _originalCreate = completionsProto.create;
57
+ _patchedProto = completionsProto;
58
+
59
+ const wrappedCreate = withInstrumentation(
60
+ async function openaiCreate(this: unknown, ...args: unknown[]): Promise<unknown> {
61
+ const original = _originalCreate as (...args: unknown[]) => Promise<unknown>;
62
+ const response = await original.apply(this, args);
63
+
64
+ const ctx = currentContext();
65
+ if (ctx) {
66
+ ctx.framework = 'openai';
67
+ ctx.nodeType = 'llm';
68
+
69
+ const resp = response as Record<string, unknown>;
70
+ const usage = resp?.usage as { prompt_tokens?: number; completion_tokens?: number } | undefined;
71
+ if (usage) {
72
+ ctx.setTokens(usage.prompt_tokens ?? 0, usage.completion_tokens ?? 0);
73
+ }
74
+ const modelName = (resp?.model as string) ?? 'unknown';
75
+ ctx.setModel(modelName, 'openai');
76
+
77
+ const firstArg = args[0] as Record<string, unknown> | undefined;
78
+ if (firstArg?.messages) {
79
+ ctx.setIO(JSON.stringify(firstArg.messages), JSON.stringify(resp?.choices));
80
+ }
81
+ }
82
+
83
+ return response;
84
+ },
85
+ { name: effectiveName, governance: options.governance },
86
+ );
87
+
88
+ completionsProto.create = wrappedCreate;
89
+ }
90
+
91
+ /**
92
+ * Restore original OpenAI methods.
93
+ */
94
+ export async function unpatchOpenAI(): Promise<void> {
95
+ if (_originalCreate === null) return;
96
+
97
+ if (_patchedProto) {
98
+ _patchedProto.create = _originalCreate;
99
+ _patchedProto = null;
100
+ }
101
+
102
+ _originalCreate = null;
103
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "outDir": "dist",
13
+ "rootDir": "src",
14
+ "sourceMap": true,
15
+ "paths": {
16
+ "@turingpulse/sdk": ["../../src/index.ts"]
17
+ }
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,100 @@
1
+ {
2
+ "id": "turingpulse",
3
+ "name": "TuringPulse Observability & Governance",
4
+ "version": "0.1.0",
5
+ "description": "Deterministic governance, observability, evaluations, and alerting for OpenClaw agents via TuringPulse",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "required": ["apiKey"],
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "apiKey": {
12
+ "type": "string",
13
+ "description": "TuringPulse API key (scoped to a project)"
14
+ },
15
+ "endpoint": {
16
+ "type": "string",
17
+ "default": "https://api.turingpulse.ai",
18
+ "description": "TuringPulse API endpoint"
19
+ },
20
+ "governance": {
21
+ "type": "object",
22
+ "properties": {
23
+ "enabled": {
24
+ "type": "boolean",
25
+ "default": true,
26
+ "description": "Enable deterministic policy checks before tool execution"
27
+ },
28
+ "failMode": {
29
+ "type": "string",
30
+ "enum": ["open", "closed"],
31
+ "default": "open",
32
+ "description": "Behavior when TuringPulse is unreachable: 'open' allows execution, 'closed' blocks"
33
+ },
34
+ "timeoutMs": {
35
+ "type": "number",
36
+ "default": 3000,
37
+ "description": "Max time to wait for policy check response before applying failMode"
38
+ },
39
+ "excludeTools": {
40
+ "type": "array",
41
+ "items": { "type": "string" },
42
+ "description": "Tool names to skip governance checks for (e.g., safe read-only tools)"
43
+ },
44
+ "scanOutboundMessages": {
45
+ "type": "boolean",
46
+ "default": false,
47
+ "description": "Scan outgoing messages for PII before delivery to chat channels"
48
+ }
49
+ }
50
+ },
51
+ "telemetry": {
52
+ "type": "object",
53
+ "properties": {
54
+ "enabled": {
55
+ "type": "boolean",
56
+ "default": true,
57
+ "description": "Enable telemetry capture and emission"
58
+ },
59
+ "batchSize": {
60
+ "type": "number",
61
+ "default": 20,
62
+ "description": "Number of events to buffer before flushing"
63
+ },
64
+ "flushIntervalMs": {
65
+ "type": "number",
66
+ "default": 5000,
67
+ "description": "Max time between flushes in milliseconds"
68
+ },
69
+ "captureMessageContent": {
70
+ "type": "boolean",
71
+ "default": false,
72
+ "description": "Whether to capture message body text (disabled by default for privacy)"
73
+ },
74
+ "captureToolParams": {
75
+ "type": "boolean",
76
+ "default": true,
77
+ "description": "Whether to capture tool call parameters"
78
+ },
79
+ "redactPatterns": {
80
+ "type": "array",
81
+ "items": { "type": "string" },
82
+ "description": "Regex patterns to redact from captured content (applied to tool params, messages)"
83
+ }
84
+ }
85
+ },
86
+ "metadata": {
87
+ "type": "object",
88
+ "additionalProperties": { "type": "string" },
89
+ "description": "Static key-value metadata attached to all events (e.g., environment, team, deployment)"
90
+ }
91
+ }
92
+ },
93
+ "uiHints": {
94
+ "apiKey": { "label": "TuringPulse API Key", "sensitive": true },
95
+ "endpoint": { "label": "API Endpoint", "placeholder": "https://api.turingpulse.ai" },
96
+ "governance.enabled": { "label": "Enable Governance" },
97
+ "governance.failMode": { "label": "Fail Mode", "placeholder": "open" },
98
+ "telemetry.captureMessageContent": { "label": "Capture Message Content (privacy risk)" }
99
+ }
100
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@turingpulse/sdk-openclaw",
3
+ "version": "1.0.1",
4
+ "description": "TuringPulse observability, governance, and alerting plugin for OpenClaw",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "lint": "tsc -p tsconfig.json --noEmit",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
19
+ },
20
+ "dependencies": {
21
+ "@turingpulse/sdk": ">=1.0.0",
22
+ "@turingpulse/interfaces": ">=1.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.7.4",
26
+ "vitest": "^3.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "typescript": ">=5.0.0"
30
+ },
31
+ "openclaw": {
32
+ "extensions": [
33
+ "./index.js"
34
+ ]
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "openclaw.plugin.json"
39
+ ],
40
+ "license": "SEE LICENSE IN LICENSE"
41
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Event buffer that batches AgentEvents and flushes them to TuringPulse.
3
+ *
4
+ * Flushes on three triggers:
5
+ * 1. Batch size reached (configurable, default 20)
6
+ * 2. Timer interval (configurable, default 5s)
7
+ * 3. Explicit flush (called on message_sent and gateway shutdown)
8
+ *
9
+ * Silently drops events on persistent failure — never crashes the host.
10
+ */
11
+
12
+ import type { AgentEvent } from '@turingpulse/interfaces';
13
+ import type { TuringPulseHttpClient } from '@turingpulse/sdk';
14
+
15
+ const MAX_BUFFER_SIZE = 500;
16
+
17
+ export class EventBuffer {
18
+ private buffer: AgentEvent[] = [];
19
+ private timer: ReturnType<typeof setInterval> | null = null;
20
+ private flushing = false;
21
+
22
+ constructor(
23
+ private readonly client: TuringPulseHttpClient,
24
+ private readonly batchSize: number,
25
+ private readonly flushIntervalMs: number,
26
+ ) {}
27
+
28
+ /** Start the periodic flush timer. Call once during plugin init. */
29
+ start(): void {
30
+ if (this.timer) return;
31
+ this.timer = setInterval(() => {
32
+ void this.flush();
33
+ }, this.flushIntervalMs);
34
+
35
+ if (typeof this.timer === 'object' && 'unref' in this.timer) {
36
+ this.timer.unref();
37
+ }
38
+ }
39
+
40
+ /** Stop the periodic flush timer and drain remaining events. */
41
+ async stop(): Promise<void> {
42
+ if (this.timer) {
43
+ clearInterval(this.timer);
44
+ this.timer = null;
45
+ }
46
+ await this.flush();
47
+ }
48
+
49
+ /**
50
+ * Add an event to the buffer. Triggers a flush if batch size is reached.
51
+ */
52
+ push(event: AgentEvent): void {
53
+ if (this.buffer.length >= MAX_BUFFER_SIZE) {
54
+ this.buffer.shift();
55
+ }
56
+ this.buffer.push(event);
57
+
58
+ if (this.buffer.length >= this.batchSize) {
59
+ void this.flush();
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Add multiple events to the buffer.
65
+ */
66
+ pushMany(events: AgentEvent[]): void {
67
+ for (const event of events) {
68
+ this.push(event);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Flush all buffered events to TuringPulse. Safe to call concurrently
74
+ * — uses a flushing guard to prevent overlapping requests.
75
+ */
76
+ async flush(): Promise<void> {
77
+ if (this.flushing || this.buffer.length === 0) return;
78
+
79
+ this.flushing = true;
80
+ const batch = this.buffer.splice(0);
81
+
82
+ try {
83
+ await this.client.emitEvents(batch);
84
+ } catch (err) {
85
+ // eslint-disable-next-line no-console
86
+ console.warn(
87
+ `[turingpulse] Failed to flush ${batch.length} events:`,
88
+ err instanceof Error ? err.message : String(err),
89
+ );
90
+ } finally {
91
+ this.flushing = false;
92
+ }
93
+ }
94
+
95
+ /** Number of events currently buffered (for health/status reporting). */
96
+ get pending(): number {
97
+ return this.buffer.length;
98
+ }
99
+ }