@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.
- package/.github/dependabot.yml +38 -0
- package/.github/workflows/ci.yml +246 -0
- package/.github/workflows/framework-compat.yml +169 -0
- package/.github/workflows/security.yml +336 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +13 -0
- package/MIGRATION.md +30 -0
- package/README.md +221 -0
- package/dist/attachments.d.ts +28 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +59 -0
- package/dist/attachments.js.map +1 -0
- package/dist/config.d.ts +72 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +126 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +163 -0
- package/dist/context.js.map +1 -0
- package/dist/decorators.d.ts +6 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +52 -0
- package/dist/decorators.js.map +1 -0
- package/dist/deploy.d.ts +89 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +203 -0
- package/dist/deploy.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventBuilder.d.ts +21 -0
- package/dist/eventBuilder.d.ts.map +1 -0
- package/dist/eventBuilder.js +127 -0
- package/dist/eventBuilder.js.map +1 -0
- package/dist/fingerprint.d.ts +158 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +339 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/governance.d.ts +47 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +104 -0
- package/dist/governance.js.map +1 -0
- package/dist/http.d.ts +62 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +181 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +40 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +31 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/integrations/mastra.d.ts +64 -0
- package/dist/integrations/mastra.d.ts.map +1 -0
- package/dist/integrations/mastra.js +256 -0
- package/dist/integrations/mastra.js.map +1 -0
- package/dist/kpi.d.ts +21 -0
- package/dist/kpi.d.ts.map +1 -0
- package/dist/kpi.js +83 -0
- package/dist/kpi.js.map +1 -0
- package/dist/llmDetector.d.ts +22 -0
- package/dist/llmDetector.d.ts.map +1 -0
- package/dist/llmDetector.js +269 -0
- package/dist/llmDetector.js.map +1 -0
- package/dist/plugin.d.ts +33 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +312 -0
- package/dist/plugin.js.map +1 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +18 -0
- package/dist/registry.js.map +1 -0
- package/dist/tracing.d.ts +10 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +30 -0
- package/dist/tracing.js.map +1 -0
- package/dist/triggerState.d.ts +5 -0
- package/dist/triggerState.d.ts.map +1 -0
- package/dist/triggerState.js +19 -0
- package/dist/triggerState.js.map +1 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +72 -0
- package/dist/utils.js.map +1 -0
- package/package.json +37 -0
- package/packages/anthropic/package.json +16 -0
- package/packages/anthropic/src/index.ts +5 -0
- package/packages/anthropic/src/wrapper.ts +102 -0
- package/packages/anthropic/tsconfig.build.json +20 -0
- package/packages/langchain/package.json +16 -0
- package/packages/langchain/src/index.ts +7 -0
- package/packages/langchain/src/wrapper.ts +51 -0
- package/packages/mastra/package.json +17 -0
- package/packages/mastra/src/index.ts +8 -0
- package/packages/mastra/src/wrapper.ts +301 -0
- package/packages/openai/package.json +16 -0
- package/packages/openai/src/index.ts +8 -0
- package/packages/openai/src/wrapper.ts +103 -0
- package/packages/openai/tsconfig.build.json +20 -0
- package/packages/openclaw/openclaw.plugin.json +100 -0
- package/packages/openclaw/package.json +41 -0
- package/packages/openclaw/src/buffer.ts +99 -0
- package/packages/openclaw/src/config.ts +139 -0
- package/packages/openclaw/src/hooks/governance.ts +267 -0
- package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
- package/packages/openclaw/src/hooks/telemetry.ts +207 -0
- package/packages/openclaw/src/index.ts +91 -0
- package/packages/openclaw/src/mapper.ts +233 -0
- package/packages/openclaw/src/session-tracker.ts +181 -0
- package/packages/openclaw/src/types.ts +220 -0
- package/packages/openclaw/tests/buffer.test.ts +148 -0
- package/packages/openclaw/tests/config.test.ts +122 -0
- package/packages/openclaw/tests/governance.test.ts +232 -0
- package/packages/openclaw/tests/mapper.test.ts +242 -0
- package/packages/openclaw/tests/session-tracker.test.ts +124 -0
- package/packages/openclaw/tsconfig.json +18 -0
- package/packages/openclaw/vitest.config.ts +8 -0
- package/packages/vercel-ai/package.json +16 -0
- package/packages/vercel-ai/src/index.ts +5 -0
- package/packages/vercel-ai/src/wrapper.ts +49 -0
- package/scripts/bump-version.sh +58 -0
- package/scripts/update-readme-compat.mjs +151 -0
- package/src/__tests__/fingerprint.test.ts +328 -0
- package/src/attachments.ts +88 -0
- package/src/config.ts +164 -0
- package/src/context.ts +258 -0
- package/src/decorators.ts +61 -0
- package/src/deploy.ts +260 -0
- package/src/errors.ts +44 -0
- package/src/eventBuilder.ts +153 -0
- package/src/fingerprint.ts +421 -0
- package/src/governance.ts +156 -0
- package/src/http.ts +241 -0
- package/src/index.ts +57 -0
- package/src/instrumentation.ts +68 -0
- package/src/integrations/mastra.ts +335 -0
- package/src/kpi.ts +112 -0
- package/src/llmDetector.ts +330 -0
- package/src/plugin.ts +384 -0
- package/src/registry.ts +27 -0
- package/src/tracing.ts +39 -0
- package/src/triggerState.ts +27 -0
- package/src/utils.ts +78 -0
- package/tests/compat/anthropic.test.ts +61 -0
- package/tests/compat/cohere.test.ts +57 -0
- package/tests/compat/google-genai.test.ts +61 -0
- package/tests/compat/langchain-openai.test.ts +41 -0
- package/tests/compat/langchain.test.ts +64 -0
- package/tests/compat/mistral.test.ts +58 -0
- package/tests/compat/openai.test.ts +71 -0
- package/tests/compat/vercel-ai.test.ts +56 -0
- package/tests/plugins/anthropic-wrapper.test.ts +120 -0
- package/tests/plugins/langchain-wrapper.test.ts +128 -0
- package/tests/plugins/openai-wrapper.test.ts +165 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { TuringPulseConfig, TuringPulseConfigOptions } from './config';
|
|
2
|
+
import { contextStorage, currentContext, ExecutionContext, generateUUID } from './context';
|
|
3
|
+
import { GovernanceBlockedError, PluginNotInitializedError, TriggerNotFoundError } from './errors';
|
|
4
|
+
import { EventBuilder } from './eventBuilder';
|
|
5
|
+
import { FingerprintBuilder, FingerprintData } from './fingerprint';
|
|
6
|
+
import { GovernanceDirective, GovernanceManager } from './governance';
|
|
7
|
+
import { PolicyCheckResult, TuringPulseHttpClient } from './http';
|
|
8
|
+
import { InstrumentationOptions, resolveOperation, tracingEnabled } from './instrumentation';
|
|
9
|
+
import { evaluateKpis } from './kpi';
|
|
10
|
+
import { detectNodeType } from './llmDetector';
|
|
11
|
+
import { TriggerRegistry } from './registry';
|
|
12
|
+
import { TracingManager } from './tracing';
|
|
13
|
+
import { flushPending } from './triggerState';
|
|
14
|
+
import { FORBIDDEN_KEYS, isAsyncIterable, isPromise, safeErrorMessage } from './utils';
|
|
15
|
+
|
|
16
|
+
export class TuringPulsePlugin {
|
|
17
|
+
readonly config: TuringPulseConfig;
|
|
18
|
+
/** @internal — used by deploy.ts and other SDK modules; not part of the public API. */
|
|
19
|
+
readonly client: TuringPulseHttpClient;
|
|
20
|
+
private readonly tracing: TracingManager;
|
|
21
|
+
private readonly governance: GovernanceManager;
|
|
22
|
+
private readonly eventBuilder: EventBuilder;
|
|
23
|
+
private readonly registry = new TriggerRegistry();
|
|
24
|
+
|
|
25
|
+
constructor(config: TuringPulseConfig) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.client = new TuringPulseHttpClient(config);
|
|
28
|
+
this.tracing = new TracingManager(config);
|
|
29
|
+
this.governance = new GovernanceManager(this.client, config);
|
|
30
|
+
this.eventBuilder = new EventBuilder(config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ------------------------------------------------------------------
|
|
34
|
+
// Trigger management
|
|
35
|
+
// ------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
registerTrigger(key: string, fn: (...args: unknown[]) => unknown, description?: string): string {
|
|
38
|
+
const namespaced = this.namespacedKey(key);
|
|
39
|
+
this.registry.register(namespaced, fn, description);
|
|
40
|
+
return namespaced;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
unregisterTrigger(key: string): void {
|
|
44
|
+
this.registry.unregister(this.namespacedKey(key));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async triggerAgent(key: string, ...args: unknown[]): Promise<unknown> {
|
|
48
|
+
const namespaced = this.namespacedKey(key);
|
|
49
|
+
const entry = this.registry.get(namespaced);
|
|
50
|
+
if (!entry) throw new TriggerNotFoundError(namespaced);
|
|
51
|
+
const result = entry.fn(...args);
|
|
52
|
+
return isPromise(result) ? result : Promise.resolve(result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
listTriggers() {
|
|
56
|
+
return this.registry.list();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ------------------------------------------------------------------
|
|
60
|
+
// Execution
|
|
61
|
+
// ------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
execute(
|
|
64
|
+
fn: (...args: unknown[]) => unknown,
|
|
65
|
+
thisArg: unknown,
|
|
66
|
+
args: unknown[],
|
|
67
|
+
options: InstrumentationOptions,
|
|
68
|
+
): unknown {
|
|
69
|
+
const context = this.buildContext(fn, args, options);
|
|
70
|
+
const spanName = resolveOperation(options, fn.name || options.agentId || 'agent');
|
|
71
|
+
const enabled = tracingEnabled(options, this.config.traceEnabled);
|
|
72
|
+
|
|
73
|
+
return this.tracing.withSpan(spanName, context.labels, enabled, () =>
|
|
74
|
+
contextStorage.run(context, () => this.invokeFunction(fn, thisArg, args, context, options)),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ------------------------------------------------------------------
|
|
79
|
+
// Pre-execution governance (HITL blocking)
|
|
80
|
+
// ------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
private resolveGovernance(options: InstrumentationOptions): GovernanceDirective | undefined {
|
|
83
|
+
const directive = options.governance;
|
|
84
|
+
const defaults = this.config.governanceDefaults;
|
|
85
|
+
const defaultHitl = defaults.hitl ?? false;
|
|
86
|
+
|
|
87
|
+
if (!directive && !defaultHitl) return undefined;
|
|
88
|
+
|
|
89
|
+
if (!directive) {
|
|
90
|
+
return new GovernanceDirective({
|
|
91
|
+
hitl: defaultHitl,
|
|
92
|
+
reviewers: defaults.reviewers,
|
|
93
|
+
escalationChannels: defaults.escalationChannels,
|
|
94
|
+
metadata: defaults.metadata ? { ...defaults.metadata } : undefined,
|
|
95
|
+
severity: defaults.severity,
|
|
96
|
+
autoEscalateAfterSeconds: defaults.autoEscalateAfterSeconds,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!directive.hitl && defaultHitl) {
|
|
101
|
+
return new GovernanceDirective({ ...directive, hitl: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return directive;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async preExecutionPolicyCheck(
|
|
108
|
+
context: ExecutionContext,
|
|
109
|
+
options: InstrumentationOptions,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const directive = this.resolveGovernance(options);
|
|
112
|
+
if (!directive?.hitl) return;
|
|
113
|
+
|
|
114
|
+
let result: PolicyCheckResult;
|
|
115
|
+
try {
|
|
116
|
+
result = await this.client.policyCheck({
|
|
117
|
+
workflowName: context.workflowName,
|
|
118
|
+
spanId: context.spanId,
|
|
119
|
+
nodeType: context.nodeType,
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (result.action === 'block') {
|
|
126
|
+
throw new GovernanceBlockedError(result.reason, result.policyIds);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ------------------------------------------------------------------
|
|
131
|
+
// Function invocation
|
|
132
|
+
// ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
private invokeFunction(
|
|
135
|
+
fn: (...args: unknown[]) => unknown,
|
|
136
|
+
thisArg: unknown,
|
|
137
|
+
args: unknown[],
|
|
138
|
+
context: ExecutionContext,
|
|
139
|
+
options: InstrumentationOptions,
|
|
140
|
+
): unknown {
|
|
141
|
+
this.recordNode(context, fn, args);
|
|
142
|
+
|
|
143
|
+
const directive = this.resolveGovernance(options);
|
|
144
|
+
const needsPreCheck = Boolean(directive?.hitl);
|
|
145
|
+
|
|
146
|
+
if (needsPreCheck) {
|
|
147
|
+
return this.preExecutionPolicyCheck(context, options)
|
|
148
|
+
.then(() => {
|
|
149
|
+
const result = fn.apply(thisArg, args);
|
|
150
|
+
return this.handleResult(result, context, options);
|
|
151
|
+
})
|
|
152
|
+
.catch((error: unknown) => {
|
|
153
|
+
context.finish(undefined, error);
|
|
154
|
+
this.popNode(context);
|
|
155
|
+
return this.finalize(context, options).then(() => { throw error; });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const result = fn.apply(thisArg, args);
|
|
161
|
+
return this.handleResult(result, context, options);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
context.finish(undefined, error);
|
|
164
|
+
this.popNode(context);
|
|
165
|
+
void this.finalize(context, options);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private handleResult(
|
|
171
|
+
result: unknown,
|
|
172
|
+
context: ExecutionContext,
|
|
173
|
+
options: InstrumentationOptions,
|
|
174
|
+
): unknown {
|
|
175
|
+
if (isAsyncIterable(result)) {
|
|
176
|
+
return this.wrapAsyncIterable(result, context, options) as unknown;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isPromise(result)) {
|
|
180
|
+
return result
|
|
181
|
+
.then((value: unknown) => {
|
|
182
|
+
if (isAsyncIterable(value)) {
|
|
183
|
+
return this.wrapAsyncIterable(value, context, options);
|
|
184
|
+
}
|
|
185
|
+
context.finish(value);
|
|
186
|
+
this.popNode(context);
|
|
187
|
+
return this.finalize(context, options).then(() => value);
|
|
188
|
+
})
|
|
189
|
+
.catch((error: unknown) => {
|
|
190
|
+
context.finish(undefined, error);
|
|
191
|
+
this.popNode(context);
|
|
192
|
+
return this.finalize(context, options).then(() => { throw error; });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
context.finish(result);
|
|
197
|
+
this.popNode(context);
|
|
198
|
+
void this.finalize(context, options);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ------------------------------------------------------------------
|
|
203
|
+
// Context building
|
|
204
|
+
// ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
private buildContext(
|
|
207
|
+
fn: (...args: unknown[]) => unknown,
|
|
208
|
+
args: unknown[],
|
|
209
|
+
options: InstrumentationOptions,
|
|
210
|
+
): ExecutionContext {
|
|
211
|
+
const triggerKey = options.triggerKey ? this.namespacedKey(options.triggerKey) : undefined;
|
|
212
|
+
const labels = this.config.mergedLabels(options.labels);
|
|
213
|
+
const metadata = { ...(options.metadata ?? {}), 'agent.function': fn.name || 'anonymous' };
|
|
214
|
+
|
|
215
|
+
let fingerprintBuilder: FingerprintBuilder | undefined;
|
|
216
|
+
if (this.config.fingerprint.enabled) {
|
|
217
|
+
fingerprintBuilder = new FingerprintBuilder(this.config.fingerprint);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const parent = currentContext();
|
|
221
|
+
const isChild = parent !== undefined;
|
|
222
|
+
|
|
223
|
+
return new ExecutionContext({
|
|
224
|
+
runId: isChild ? parent.runId : generateUUID(),
|
|
225
|
+
agentId: options.agentId,
|
|
226
|
+
operation: resolveOperation(options, fn.name || 'agent-operation'),
|
|
227
|
+
triggerKey,
|
|
228
|
+
hiddenEntrypoint: Boolean(options.hiddenEntrypoint),
|
|
229
|
+
labels,
|
|
230
|
+
metadata,
|
|
231
|
+
args,
|
|
232
|
+
fingerprintBuilder,
|
|
233
|
+
workflowId: options.agentId,
|
|
234
|
+
workflowName: options.name || options.agentId || this.config.workflowName,
|
|
235
|
+
parentSpanId: isChild ? parent.spanId : undefined,
|
|
236
|
+
depth: isChild ? parent.depth + 1 : 0,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ------------------------------------------------------------------
|
|
241
|
+
// Streaming — wrap async iterables so span covers full iteration
|
|
242
|
+
// ------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
private wrapAsyncIterable(
|
|
245
|
+
iterable: AsyncIterable<unknown>,
|
|
246
|
+
context: ExecutionContext,
|
|
247
|
+
options: InstrumentationOptions,
|
|
248
|
+
): AsyncIterable<unknown> {
|
|
249
|
+
const self = this;
|
|
250
|
+
async function* wrappedGenerator() {
|
|
251
|
+
const chunks: unknown[] = [];
|
|
252
|
+
try {
|
|
253
|
+
for await (const chunk of iterable) {
|
|
254
|
+
chunks.push(chunk);
|
|
255
|
+
yield chunk;
|
|
256
|
+
}
|
|
257
|
+
context.finish(chunks);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
context.finish(undefined, error);
|
|
260
|
+
throw error;
|
|
261
|
+
} finally {
|
|
262
|
+
self.popNode(context);
|
|
263
|
+
await self.finalize(context, options);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return wrappedGenerator();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ------------------------------------------------------------------
|
|
270
|
+
// Finalize — emit event, governance, fingerprint
|
|
271
|
+
// ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
private async finalize(context: ExecutionContext, options: InstrumentationOptions): Promise<void> {
|
|
274
|
+
const kpiResults = evaluateKpis(options.kpis ?? [], context);
|
|
275
|
+
const event = this.eventBuilder.build(context, options, kpiResults);
|
|
276
|
+
try {
|
|
277
|
+
await this.client.emitEvents([event]);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// eslint-disable-next-line no-console
|
|
280
|
+
console.warn('Failed to emit TuringPulse event:', safeErrorMessage(error));
|
|
281
|
+
}
|
|
282
|
+
if (!(context.error instanceof GovernanceBlockedError)) {
|
|
283
|
+
try {
|
|
284
|
+
await this.governance.enforce(options.governance, context, kpiResults);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
// eslint-disable-next-line no-console
|
|
287
|
+
console.warn('Failed to enforce governance directive:', safeErrorMessage(error));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (context.fingerprintBuilder) {
|
|
291
|
+
await this.sendFingerprint(context);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ------------------------------------------------------------------
|
|
296
|
+
// Fingerprinting
|
|
297
|
+
// ------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
private recordNode(context: ExecutionContext, fn: (...args: unknown[]) => unknown, args: unknown[]): void {
|
|
300
|
+
if (!context.fingerprintBuilder) return;
|
|
301
|
+
const funcName = fn.name || 'anonymous';
|
|
302
|
+
const kwargs: Record<string, unknown> = Object.create(null);
|
|
303
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
304
|
+
for (const [k, v] of Object.entries(args[0] as Record<string, unknown>)) {
|
|
305
|
+
if (!FORBIDDEN_KEYS.has(k)) kwargs[k] = v;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const [nodeType, llmInfo] = detectNodeType(funcName, funcName, args, kwargs);
|
|
309
|
+
if (llmInfo) {
|
|
310
|
+
context.recordNode(funcName, nodeType, llmInfo.config, llmInfo.prompt);
|
|
311
|
+
} else {
|
|
312
|
+
context.recordNode(funcName, nodeType);
|
|
313
|
+
}
|
|
314
|
+
context.fingerprintBuilder.pushParent(funcName);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private popNode(context: ExecutionContext): void {
|
|
318
|
+
context.fingerprintBuilder?.popParent();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private async sendFingerprint(context: ExecutionContext): Promise<void> {
|
|
322
|
+
if (!this.config.fingerprint.enabled || !context.fingerprintBuilder) return;
|
|
323
|
+
if (context.error && !this.config.fingerprint.sendOnFailure) return;
|
|
324
|
+
|
|
325
|
+
const payload = {
|
|
326
|
+
run_id: context.runId,
|
|
327
|
+
workflow_id: context.workflowId || context.agentId,
|
|
328
|
+
fingerprint: context.fingerprintBuilder.getFingerprint(),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const emit = () =>
|
|
332
|
+
this.client
|
|
333
|
+
.postFingerprint(payload as unknown as Record<string, unknown>)
|
|
334
|
+
.catch((error: unknown) => {
|
|
335
|
+
// eslint-disable-next-line no-console
|
|
336
|
+
console.warn('Failed to send fingerprint:', safeErrorMessage(error));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (this.config.fingerprint.sendAsync) {
|
|
340
|
+
void emit();
|
|
341
|
+
} else {
|
|
342
|
+
await emit();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ------------------------------------------------------------------
|
|
347
|
+
// Helpers
|
|
348
|
+
// ------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
private namespacedKey(key: string): string {
|
|
351
|
+
return key.includes(':') ? key : `${this.config.triggerNamespace}:${key}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Module-level singleton
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
let pluginInstance: TuringPulsePlugin | undefined;
|
|
360
|
+
|
|
361
|
+
export function init(options: TuringPulseConfig | TuringPulseConfigOptions): TuringPulsePlugin {
|
|
362
|
+
const config = options instanceof TuringPulseConfig ? options : new TuringPulseConfig(options);
|
|
363
|
+
pluginInstance = new TuringPulsePlugin(config);
|
|
364
|
+
flushPending(pluginInstance);
|
|
365
|
+
return pluginInstance;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function getInstance(): TuringPulsePlugin {
|
|
369
|
+
if (!pluginInstance) {
|
|
370
|
+
const apiKey = typeof process !== 'undefined' ? process.env?.TP_API_KEY : undefined;
|
|
371
|
+
const workflowName = typeof process !== 'undefined' ? process.env?.TP_WORKFLOW_NAME : undefined;
|
|
372
|
+
|
|
373
|
+
if (apiKey && workflowName) {
|
|
374
|
+
// eslint-disable-next-line no-console
|
|
375
|
+
console.info('Auto-initializing TuringPulse SDK from environment variables');
|
|
376
|
+
return init({ apiKey, workflowName });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
throw new PluginNotInitializedError();
|
|
380
|
+
}
|
|
381
|
+
return pluginInstance;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export type TuringPulseDecoratorTarget = (...args: unknown[]) => unknown;
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface TriggerEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
fn: (...args: unknown[]) => unknown;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class TriggerRegistry {
|
|
8
|
+
private readonly entries = new Map<string, TriggerEntry>();
|
|
9
|
+
|
|
10
|
+
register(key: string, fn: (...args: unknown[]) => unknown, description?: string): void {
|
|
11
|
+
this.entries.set(key, { key, fn, description });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
unregister(key: string): void {
|
|
15
|
+
this.entries.delete(key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(key: string): TriggerEntry | undefined {
|
|
19
|
+
return this.entries.get(key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
list(): TriggerEntry[] {
|
|
23
|
+
return Array.from(this.entries.values());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
package/src/tracing.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
import { TuringPulseConfig } from './config';
|
|
4
|
+
import { isPromise } from './utils';
|
|
5
|
+
|
|
6
|
+
type Attributes = Record<string, string>;
|
|
7
|
+
|
|
8
|
+
export class TracingManager {
|
|
9
|
+
private readonly tracer;
|
|
10
|
+
|
|
11
|
+
constructor(private readonly config: TuringPulseConfig) {
|
|
12
|
+
this.tracer = trace.getTracer(config.traceServiceName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
withSpan<T>(name: string, attributes: Attributes, enabled: boolean, fn: () => T): T {
|
|
16
|
+
if (!this.config.traceEnabled || !enabled) {
|
|
17
|
+
return fn();
|
|
18
|
+
}
|
|
19
|
+
return this.tracer.startActiveSpan(
|
|
20
|
+
name,
|
|
21
|
+
{ attributes },
|
|
22
|
+
(span) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = fn();
|
|
25
|
+
if (isPromise(result)) {
|
|
26
|
+
return result.finally(() => span.end()) as T;
|
|
27
|
+
}
|
|
28
|
+
span.end();
|
|
29
|
+
return result;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
span.recordException(error as Error);
|
|
32
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
33
|
+
span.end();
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { markTriggerRegistered } from './utils';
|
|
2
|
+
|
|
3
|
+
interface PendingTrigger {
|
|
4
|
+
key: string;
|
|
5
|
+
fn: (...args: unknown[]) => unknown;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const pending: PendingTrigger[] = [];
|
|
10
|
+
|
|
11
|
+
export function queueTrigger(key: string, fn: (...args: unknown[]) => unknown, description?: string): void {
|
|
12
|
+
pending.push({ key, fn, description });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function flushPending(plugin: { registerTrigger: (key: string, fn: (...args: unknown[]) => unknown, description?: string) => string }): void {
|
|
16
|
+
if (!pending.length) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
while (pending.length) {
|
|
20
|
+
const item = pending.shift();
|
|
21
|
+
if (!item) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
plugin.registerTrigger(item.key, item.fn, item.description);
|
|
25
|
+
markTriggerRegistered(item.fn);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a camelCase string to snake_case.
|
|
3
|
+
* e.g. "runId" → "run_id", "durationMs" → "duration_ms"
|
|
4
|
+
*/
|
|
5
|
+
export function toSnake(s: string): string {
|
|
6
|
+
return s.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Type guard for thenables (Promises). Shared by plugin and tracing. */
|
|
10
|
+
export function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
|
|
11
|
+
return typeof (value as Promise<T>)?.then === 'function';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Type guard for async iterables (streaming responses). */
|
|
15
|
+
export function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
16
|
+
return (
|
|
17
|
+
value !== null &&
|
|
18
|
+
typeof value === 'object' &&
|
|
19
|
+
typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Safe access to process.env — returns undefined in browser environments. */
|
|
24
|
+
export function safeEnv(key: string): string | undefined {
|
|
25
|
+
try {
|
|
26
|
+
return typeof process !== 'undefined' && process.env ? process.env[key] : undefined;
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keys that must never be assigned as own properties to prevent prototype pollution. */
|
|
33
|
+
export const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
34
|
+
|
|
35
|
+
/** Extract only the safe message string from an unknown error. */
|
|
36
|
+
export function safeErrorMessage(err: unknown): string {
|
|
37
|
+
if (err instanceof Error) return err.message;
|
|
38
|
+
if (typeof err === 'string') return err;
|
|
39
|
+
return 'unknown error';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TRIGGER_PROP = '__turingpulseTriggerRegistered';
|
|
43
|
+
|
|
44
|
+
/** Mark a function as having its trigger registered. */
|
|
45
|
+
export function markTriggerRegistered(fn: (...args: unknown[]) => unknown): void {
|
|
46
|
+
Object.defineProperty(fn, TRIGGER_PROP, {
|
|
47
|
+
value: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check whether a function's trigger has been registered. */
|
|
54
|
+
export function isTriggerRegistered(fn: (...args: unknown[]) => unknown): boolean {
|
|
55
|
+
return Boolean((fn as unknown as Record<string, unknown>)[TRIGGER_PROP]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Deep-convert all object keys from camelCase to snake_case.
|
|
60
|
+
* Arrays are recursed; primitives are returned as-is.
|
|
61
|
+
* This is necessary because the ingestion service (Python) expects
|
|
62
|
+
* snake_case keys while the TypeScript interfaces use camelCase.
|
|
63
|
+
*/
|
|
64
|
+
export function toSnakeCaseDeep(obj: unknown): unknown {
|
|
65
|
+
if (Array.isArray(obj)) {
|
|
66
|
+
return obj.map(toSnakeCaseDeep);
|
|
67
|
+
}
|
|
68
|
+
if (obj !== null && typeof obj === 'object' && !(obj instanceof Date)) {
|
|
69
|
+
const out: Record<string, unknown> = Object.create(null);
|
|
70
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
71
|
+
const snaked = toSnake(k);
|
|
72
|
+
if (FORBIDDEN_KEYS.has(k) || FORBIDDEN_KEYS.has(snaked)) continue;
|
|
73
|
+
out[snaked] = toSnakeCaseDeep(v);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
return obj;
|
|
78
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility tests for the Anthropic TypeScript SDK.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { FingerprintBuilder, classifyConfigKey } from '../../src/fingerprint.js';
|
|
6
|
+
|
|
7
|
+
// ── Import guard ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
describe('Anthropic import', () => {
|
|
10
|
+
it('anthropic package is importable', async () => {
|
|
11
|
+
const mod = await import('@anthropic-ai/sdk');
|
|
12
|
+
expect(mod).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ── Key classification ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('Anthropic key classification', () => {
|
|
19
|
+
const modelKeys = ['model', 'temperature', 'top_p', 'top_k', 'max_tokens', 'stop_sequences'];
|
|
20
|
+
const camelKeys = ['topP', 'topK', 'maxTokens', 'stopSequences'];
|
|
21
|
+
const generalKeys = ['stream', 'system', 'metadata'];
|
|
22
|
+
|
|
23
|
+
it.each(modelKeys)('classifies %s as model', (key) => {
|
|
24
|
+
expect(classifyConfigKey(key)).toBe('model');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each(camelKeys)('classifies camelCase %s as model', (key) => {
|
|
28
|
+
expect(classifyConfigKey(key)).toBe('model');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each(generalKeys)('classifies %s as general', (key) => {
|
|
32
|
+
expect(classifyConfigKey(key)).toBe('general');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── Fingerprint hashing ──────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe('Anthropic fingerprint', () => {
|
|
39
|
+
it('produces model hash', () => {
|
|
40
|
+
const builder = new FingerprintBuilder({ captureConfigs: true, capturePrompts: true });
|
|
41
|
+
builder.recordNode('claude', 'llm', {
|
|
42
|
+
model: 'claude-3-sonnet-20240229',
|
|
43
|
+
temperature: 0.5,
|
|
44
|
+
max_tokens: 1024,
|
|
45
|
+
top_k: 40,
|
|
46
|
+
});
|
|
47
|
+
const fp = builder.getFingerprint();
|
|
48
|
+
expect(fp.node_model_hashes['claude']).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('model swap changes model hash', () => {
|
|
52
|
+
const before = new FingerprintBuilder({ captureConfigs: true, capturePrompts: true });
|
|
53
|
+
const after = new FingerprintBuilder({ captureConfigs: true, capturePrompts: true });
|
|
54
|
+
|
|
55
|
+
before.recordNode('llm', 'llm', { model: 'claude-3-haiku-20240307', max_tokens: 1024 });
|
|
56
|
+
after.recordNode('llm', 'llm', { model: 'claude-3-sonnet-20240229', max_tokens: 1024 });
|
|
57
|
+
|
|
58
|
+
expect(before.getFingerprint().node_model_hashes['llm'])
|
|
59
|
+
.not.toBe(after.getFingerprint().node_model_hashes['llm']);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility tests for the Cohere TypeScript SDK.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { FingerprintBuilder, classifyConfigKey } from '../../src/fingerprint.js';
|
|
6
|
+
|
|
7
|
+
// ── Import guard ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
describe('Cohere import', () => {
|
|
10
|
+
it('cohere-ai package is importable', async () => {
|
|
11
|
+
const mod = await import('cohere-ai');
|
|
12
|
+
expect(mod).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ── Key classification ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('Cohere key classification', () => {
|
|
19
|
+
const modelKeys = ['model', 'temperature', 'max_tokens', 'top_p', 'top_k', 'p', 'k', 'frequency_penalty', 'presence_penalty', 'stop_sequences'];
|
|
20
|
+
const generalKeys = ['stream', 'preamble', 'connectors'];
|
|
21
|
+
|
|
22
|
+
it.each(modelKeys)('classifies %s as model', (key) => {
|
|
23
|
+
expect(classifyConfigKey(key)).toBe('model');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it.each(generalKeys)('classifies %s as general', (key) => {
|
|
27
|
+
expect(classifyConfigKey(key)).toBe('general');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ── Fingerprint hashing ──────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe('Cohere fingerprint', () => {
|
|
34
|
+
it('produces model hash with p/k aliases', () => {
|
|
35
|
+
const builder = new FingerprintBuilder({ captureConfigs: true, capturePrompts: true });
|
|
36
|
+
builder.recordNode('cohere_chat', 'llm', {
|
|
37
|
+
model: 'command-r-plus',
|
|
38
|
+
temperature: 0.3,
|
|
39
|
+
p: 0.75,
|
|
40
|
+
k: 50,
|
|
41
|
+
stream: true,
|
|
42
|
+
});
|
|
43
|
+
const fp = builder.getFingerprint();
|
|
44
|
+
expect(fp.node_model_hashes['cohere_chat']).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('p/k change alters model hash', () => {
|
|
48
|
+
const before = new FingerprintBuilder({ captureConfigs: true, capturePrompts: true });
|
|
49
|
+
const after = new FingerprintBuilder({ captureConfigs: true, capturePrompts: true });
|
|
50
|
+
|
|
51
|
+
before.recordNode('llm', 'llm', { model: 'command-r-plus', p: 0.75, k: 50 });
|
|
52
|
+
after.recordNode('llm', 'llm', { model: 'command-r-plus', p: 0.9, k: 10 });
|
|
53
|
+
|
|
54
|
+
expect(before.getFingerprint().node_model_hashes['llm'])
|
|
55
|
+
.not.toBe(after.getFingerprint().node_model_hashes['llm']);
|
|
56
|
+
});
|
|
57
|
+
});
|