@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
package/src/config.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { DEFAULT_FINGERPRINT_CONFIG, FingerprintConfig } from './fingerprint';
2
+
3
+ export type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
4
+
5
+ const CRLF_RE = /[\r\n\x00]/g;
6
+
7
+ function stripControlChars(value: string): string {
8
+ return value.replace(CRLF_RE, '');
9
+ }
10
+
11
+ const BLOCKED_HOSTS = /^(127\.\d{1,3}\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost|\[::1\]|\[fd[0-9a-f]{2}:|\[fe80:)$/i;
12
+
13
+ function checkEndpointSsrf(endpoint: string): void {
14
+ try {
15
+ const parsed = new URL(endpoint);
16
+ if (BLOCKED_HOSTS.test(parsed.hostname)) {
17
+ throw new Error(
18
+ 'TuringPulse endpoint must not point to private/internal networks.',
19
+ );
20
+ }
21
+ } catch (e) {
22
+ if (e instanceof Error && e.message.includes('private/internal')) throw e;
23
+ }
24
+ }
25
+
26
+ export interface GovernanceDefaults {
27
+ hitl?: boolean;
28
+ reviewers?: string[];
29
+ escalationChannels?: string[];
30
+ metadata?: Record<string, string>;
31
+ severity?: string;
32
+ autoEscalateAfterSeconds?: number;
33
+ }
34
+
35
+ /**
36
+ * Configuration options for the TuringPulse SDK.
37
+ *
38
+ * Required:
39
+ * apiKey - SDK API key. The backend resolves tenant and project from it.
40
+ * workflowName - Display name for this workflow.
41
+ *
42
+ * Optional:
43
+ * endpoint - Base URL for the TuringPulse API (default: https://api.turingpulse.ai).
44
+ */
45
+ export interface TuringPulseConfigOptions {
46
+ /** SDK API key (required). */
47
+ apiKey: string;
48
+ /** Workflow display name (required). */
49
+ workflowName: string;
50
+ /** Base URL for the TuringPulse API. Override for self-hosted or staging. */
51
+ endpoint?: string;
52
+ defaultLabels?: Record<string, string>;
53
+ traceEnabled?: boolean;
54
+ traceServiceName?: string;
55
+ triggerNamespace?: string;
56
+ timeoutMs?: number;
57
+ maxRetries?: number;
58
+ fetchImpl?: FetchLike;
59
+ governanceDefaults?: GovernanceDefaults;
60
+ fingerprint?: FingerprintConfig;
61
+ captureArguments?: boolean;
62
+ captureReturnValue?: boolean;
63
+ maxSerializedFieldLength?: number;
64
+ redactFields?: string[];
65
+ }
66
+
67
+ export const DEFAULT_ENDPOINT = 'https://api.turingpulse.ai';
68
+
69
+ interface ResolvedGovernanceDefaults {
70
+ hitl: boolean;
71
+ reviewers: string[];
72
+ escalationChannels: string[];
73
+ metadata: Record<string, string>;
74
+ severity: string;
75
+ autoEscalateAfterSeconds: number | undefined;
76
+ }
77
+
78
+ const DEFAULT_GOVERNANCE: ResolvedGovernanceDefaults = {
79
+ hitl: false,
80
+ reviewers: [],
81
+ escalationChannels: [],
82
+ metadata: {},
83
+ severity: 'medium',
84
+ autoEscalateAfterSeconds: undefined,
85
+ };
86
+
87
+ export class TuringPulseConfig {
88
+ readonly apiKey: string;
89
+ readonly workflowName: string;
90
+ readonly endpoint: string;
91
+ readonly defaultLabels: Record<string, string>;
92
+ readonly traceEnabled: boolean;
93
+ readonly traceServiceName: string;
94
+ readonly triggerNamespace: string;
95
+ readonly timeoutMs: number;
96
+ readonly maxRetries: number;
97
+ readonly fetchImpl?: FetchLike;
98
+ readonly governanceDefaults: ResolvedGovernanceDefaults;
99
+ readonly fingerprint: Required<FingerprintConfig>;
100
+ readonly captureArguments: boolean;
101
+ readonly captureReturnValue: boolean;
102
+ readonly maxSerializedFieldLength: number;
103
+ readonly redactFields: string[];
104
+
105
+ constructor(options: TuringPulseConfigOptions) {
106
+ if (!options.apiKey || !options.apiKey.trim()) {
107
+ throw new Error(
108
+ 'TuringPulse apiKey is required and cannot be empty. ' +
109
+ 'Set the TP_API_KEY environment variable or pass apiKey to init().',
110
+ );
111
+ }
112
+ this.apiKey = stripControlChars(options.apiKey.trim());
113
+
114
+ if (!options.workflowName || !options.workflowName.trim()) {
115
+ throw new Error(
116
+ 'TuringPulse workflowName is required and cannot be empty. ' +
117
+ 'Pass workflowName to init(). Example: init({ apiKey: "...", workflowName: "My Agent" })',
118
+ );
119
+ }
120
+ this.workflowName = options.workflowName.trim();
121
+
122
+ const raw = stripControlChars(options.endpoint ?? DEFAULT_ENDPOINT);
123
+ this.endpoint = raw.replace(/\/$/, '');
124
+ if (this.endpoint && !/^https?:\/\//.test(this.endpoint)) {
125
+ throw new Error('TuringPulse endpoint must start with https:// or http://');
126
+ }
127
+ checkEndpointSsrf(this.endpoint);
128
+ if (this.endpoint.startsWith('http://')) {
129
+ // eslint-disable-next-line no-console
130
+ console.warn(
131
+ 'TuringPulse endpoint uses http:// \u2014 API key will be sent in cleartext. Use https:// in production.',
132
+ );
133
+ }
134
+
135
+ this.defaultLabels = { ...(options.defaultLabels ?? {}) };
136
+ this.traceEnabled = options.traceEnabled ?? true;
137
+ this.traceServiceName = options.traceServiceName ?? 'turingpulse.sdk';
138
+ this.triggerNamespace = options.triggerNamespace ?? 'default';
139
+ this.timeoutMs = Math.min(options.timeoutMs ?? 10_000, 120_000);
140
+ this.maxRetries = options.maxRetries ?? 3;
141
+ this.fetchImpl = options.fetchImpl;
142
+ this.governanceDefaults = {
143
+ hitl: options.governanceDefaults?.hitl ?? DEFAULT_GOVERNANCE.hitl,
144
+ reviewers: options.governanceDefaults?.reviewers ?? DEFAULT_GOVERNANCE.reviewers,
145
+ escalationChannels: options.governanceDefaults?.escalationChannels ?? DEFAULT_GOVERNANCE.escalationChannels,
146
+ metadata: { ...DEFAULT_GOVERNANCE.metadata, ...(options.governanceDefaults?.metadata ?? {}) },
147
+ severity: options.governanceDefaults?.severity ?? DEFAULT_GOVERNANCE.severity,
148
+ autoEscalateAfterSeconds:
149
+ options.governanceDefaults?.autoEscalateAfterSeconds ?? DEFAULT_GOVERNANCE.autoEscalateAfterSeconds,
150
+ };
151
+ this.fingerprint = {
152
+ ...DEFAULT_FINGERPRINT_CONFIG,
153
+ ...(options.fingerprint ?? {}),
154
+ };
155
+ this.captureArguments = options.captureArguments ?? false;
156
+ this.captureReturnValue = options.captureReturnValue ?? false;
157
+ this.maxSerializedFieldLength = options.maxSerializedFieldLength ?? 50_000;
158
+ this.redactFields = options.redactFields ?? [];
159
+ }
160
+
161
+ mergedLabels(labels?: Record<string, string>): Record<string, string> {
162
+ return { ...this.defaultLabels, ...(labels ?? {}) };
163
+ }
164
+ }
package/src/context.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ import type { FingerprintBuilder } from './fingerprint';
4
+ import { GovernanceBlockedError } from './errors';
5
+
6
+ export function generateUUID(): string {
7
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
8
+ return globalThis.crypto.randomUUID();
9
+ }
10
+ try {
11
+ return randomUUID();
12
+ } catch {
13
+ if (typeof globalThis.crypto?.getRandomValues === 'function') {
14
+ const bytes = new Uint8Array(16);
15
+ globalThis.crypto.getRandomValues(bytes);
16
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
17
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
18
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
19
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
20
+ }
21
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
22
+ const r = (Math.random() * 16) | 0;
23
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
24
+ });
25
+ }
26
+ }
27
+
28
+ /** Structured tool call record matching the shared ToolCallInfo model. */
29
+ export interface ToolCallRecord {
30
+ toolName: string;
31
+ toolArgs: Record<string, unknown>;
32
+ toolResult?: string;
33
+ toolId?: string;
34
+ success: boolean;
35
+ errorMessage?: string;
36
+ }
37
+
38
+ const AsyncLocalStorageCtor: (new <T>() => AsyncLocalStorage<T>) | undefined =
39
+ AsyncLocalStorage as unknown as (new <T>() => AsyncLocalStorage<T>) | undefined;
40
+
41
+ /**
42
+ * Initialization options for ExecutionContext.
43
+ *
44
+ * REFACTORED:
45
+ * - Added workflowName for display name (from @instrument(name="..."))
46
+ * - agentId is kept for backward compatibility
47
+ * - Backend auto-registers workflows based on workflowName
48
+ */
49
+ export interface ExecutionContextInit {
50
+ runId: string;
51
+ /** @deprecated Use workflowName instead */
52
+ agentId?: string;
53
+ operation: string;
54
+ triggerKey?: string;
55
+ hiddenEntrypoint?: boolean;
56
+ labels: Record<string, string>;
57
+ metadata: Record<string, string>;
58
+ args: unknown[];
59
+ fingerprintBuilder?: FingerprintBuilder;
60
+ /** @deprecated Backend assigns UUID */
61
+ workflowId?: string;
62
+ /** Display name from @instrument(name="...") */
63
+ workflowName?: string;
64
+ /** Unique identifier for this span (auto-generated if not provided). */
65
+ spanId?: string;
66
+ /** Parent span ID for trace hierarchy (root spans have no parent). */
67
+ parentSpanId?: string;
68
+ /** Nesting depth: 0 for root, parent.depth + 1 for children. */
69
+ depth?: number;
70
+ }
71
+
72
+ /**
73
+ * Execution context for instrumented functions.
74
+ *
75
+ * REFACTORED:
76
+ * - workflowName: Display name from @instrument(name="...") - sent to backend
77
+ * - agentId: DEPRECATED - kept for backward compatibility
78
+ * - Backend auto-registers workflows based on workflowName and assigns UUID
79
+ */
80
+ export class ExecutionContext {
81
+ readonly runId: string;
82
+ /** @deprecated Use workflowName instead */
83
+ readonly agentId: string;
84
+ readonly operation: string;
85
+ readonly triggerKey?: string;
86
+ readonly hiddenEntrypoint: boolean;
87
+ readonly labels: Record<string, string>;
88
+ readonly metadata: Record<string, string>;
89
+ readonly args: unknown[];
90
+ readonly startedAt: Date;
91
+ readonly fingerprintBuilder?: FingerprintBuilder;
92
+ /** @deprecated Backend assigns UUID */
93
+ readonly workflowId?: string;
94
+ /** Display name from @instrument(name="...") - sent to backend */
95
+ readonly workflowName?: string;
96
+ completedAt?: Date;
97
+ error?: unknown;
98
+ result?: unknown;
99
+ status: 'pending' | 'success' | 'error' | 'blocked' = 'pending';
100
+
101
+ // ── Structural fields for trace hierarchy ──
102
+ readonly spanId: string;
103
+ readonly parentSpanId?: string;
104
+ readonly depth: number;
105
+
106
+ // ── Telemetry fields (parity with Python SDK) ──
107
+ tokensInput = 0;
108
+ tokensOutput = 0;
109
+ costUsd = 0;
110
+ model?: string;
111
+ provider?: string;
112
+ nodeType?: string;
113
+ framework?: string;
114
+ promptText?: string;
115
+ systemPrompt?: string;
116
+ inputData?: string;
117
+ outputData?: string;
118
+ availableTools?: string[];
119
+ toolCalls: ToolCallRecord[] = [];
120
+ attachments: Record<string, unknown>[] = [];
121
+ inputContext?: string; // RAG retrieval context for faithfulness evals
122
+
123
+ constructor(init: ExecutionContextInit) {
124
+ this.runId = init.runId;
125
+ this.agentId = init.agentId ?? init.workflowName ?? '';
126
+ this.operation = init.operation;
127
+ this.triggerKey = init.triggerKey;
128
+ this.hiddenEntrypoint = Boolean(init.hiddenEntrypoint);
129
+ this.labels = init.labels;
130
+ this.metadata = init.metadata;
131
+ this.args = init.args;
132
+ this.startedAt = new Date();
133
+ this.fingerprintBuilder = init.fingerprintBuilder;
134
+ this.workflowId = init.workflowId ?? init.agentId;
135
+ this.workflowName = init.workflowName ?? init.agentId;
136
+
137
+ // Structural fields
138
+ this.spanId = init.spanId ?? generateUUID();
139
+ this.parentSpanId = init.parentSpanId;
140
+ this.depth = init.depth ?? 0;
141
+ }
142
+
143
+ finish(result?: unknown, error?: unknown): void {
144
+ this.completedAt = new Date();
145
+ this.result = result;
146
+ this.error = error;
147
+ if (error) {
148
+ this.status = error instanceof GovernanceBlockedError ? 'blocked' : 'error';
149
+ } else {
150
+ this.status = 'success';
151
+ }
152
+ }
153
+
154
+ get durationMs(): number | undefined {
155
+ if (!this.completedAt) {
156
+ return undefined;
157
+ }
158
+ return this.completedAt.getTime() - this.startedAt.getTime();
159
+ }
160
+
161
+ // ── Telemetry setters (matching Python SDK API) ──
162
+
163
+ /** Record token counts for this span. */
164
+ setTokens(input: number, output: number): void {
165
+ this.tokensInput = input;
166
+ this.tokensOutput = output;
167
+ }
168
+
169
+ /** Record LLM cost in USD. */
170
+ setCost(cost: number): void {
171
+ this.costUsd = cost;
172
+ }
173
+
174
+ /** Record the LLM model and provider used. */
175
+ setModel(model: string, provider?: string): void {
176
+ this.model = model;
177
+ if (provider) this.provider = provider;
178
+ }
179
+
180
+ /** Record prompt and optional system prompt. */
181
+ setPrompt(prompt: string, systemPrompt?: string): void {
182
+ this.promptText = prompt;
183
+ if (systemPrompt) this.systemPrompt = systemPrompt;
184
+ }
185
+
186
+ /** Record input/output data for this span. */
187
+ setIO(inputData?: string, outputData?: string): void {
188
+ if (inputData !== undefined) this.inputData = inputData;
189
+ if (outputData !== undefined) this.outputData = outputData;
190
+ }
191
+
192
+ /** Record RAG retrieval context for faithfulness evaluation. */
193
+ setInputContext(context: string): void {
194
+ this.inputContext = context.slice(0, 500_000);
195
+ }
196
+
197
+ /** Add a structured tool call record (capped at 200 per span). */
198
+ addToolCall(record: ToolCallRecord): void {
199
+ if (this.toolCalls.length >= 200) return;
200
+ this.toolCalls.push(record);
201
+ }
202
+
203
+ /**
204
+ * Record a node in the fingerprint.
205
+ *
206
+ * Called automatically during instrumentation to track workflow structure.
207
+ *
208
+ * @param name - Node name (typically the function name)
209
+ * @param nodeType - Type of node (llm, tool, function, retriever, etc.)
210
+ * @param config - Configuration dict for the node
211
+ * @param prompt - System prompt for LLM nodes
212
+ */
213
+ recordNode(
214
+ name: string,
215
+ nodeType: string,
216
+ config?: unknown,
217
+ prompt?: string,
218
+ ): void {
219
+ if (this.fingerprintBuilder) {
220
+ this.fingerprintBuilder.recordNode(name, nodeType, config, prompt);
221
+ }
222
+ }
223
+ }
224
+
225
+ class ContextStorage {
226
+ private readonly storage = AsyncLocalStorageCtor ? new AsyncLocalStorageCtor<ExecutionContext>() : undefined;
227
+ private fallback?: ExecutionContext;
228
+
229
+ run<T>(context: ExecutionContext, fn: () => T): T {
230
+ if (this.storage) {
231
+ return this.storage.run(context, fn);
232
+ }
233
+ const previous = this.fallback;
234
+ this.fallback = context;
235
+ const result = fn();
236
+ if (typeof (result as Promise<unknown>)?.then === 'function') {
237
+ return (result as unknown as Promise<unknown>).finally(() => {
238
+ this.fallback = previous;
239
+ }) as T;
240
+ }
241
+ this.fallback = previous;
242
+ return result;
243
+ }
244
+
245
+ get(): ExecutionContext | undefined {
246
+ if (this.storage) {
247
+ return this.storage.getStore() ?? undefined;
248
+ }
249
+ return this.fallback;
250
+ }
251
+ }
252
+
253
+ export const contextStorage = new ContextStorage();
254
+
255
+ export function currentContext(): ExecutionContext | undefined {
256
+ return contextStorage.get();
257
+ }
258
+
@@ -0,0 +1,61 @@
1
+ import { PluginNotInitializedError } from './errors';
2
+ import { InstrumentationOptions, validateOptions } from './instrumentation';
3
+ import { TuringPulsePlugin, getInstance } from './plugin';
4
+ import { queueTrigger } from './triggerState';
5
+ import { isTriggerRegistered, markTriggerRegistered } from './utils';
6
+
7
+ type Method = (...args: unknown[]) => unknown;
8
+
9
+ export function instrument(options: InstrumentationOptions) {
10
+ return function instrumentDecorator(
11
+ _target: unknown,
12
+ _propertyKey: string | symbol,
13
+ descriptor: PropertyDescriptor,
14
+ ): void {
15
+ if (!descriptor || typeof descriptor.value !== 'function') {
16
+ throw new Error('@instrument can only be applied to methods');
17
+ }
18
+ descriptor.value = createInstrumentedFunction(descriptor.value, options);
19
+ };
20
+ }
21
+
22
+ export function withInstrumentation<T extends Method>(fn: T, options: InstrumentationOptions): T {
23
+ return createInstrumentedFunction(fn, options);
24
+ }
25
+
26
+ function createInstrumentedFunction<T extends Method>(fn: T, options: InstrumentationOptions): T {
27
+ validateOptions(options);
28
+ const wrapper = function instrumented(this: unknown, ...args: unknown[]) {
29
+ const plugin = getInstance();
30
+ ensureTriggerRegistration(plugin, options, wrapper);
31
+ return plugin.execute(fn, this, args, options);
32
+ };
33
+
34
+ maybeRegisterTrigger(options, wrapper);
35
+ return wrapper as T;
36
+ }
37
+
38
+ function maybeRegisterTrigger(options: InstrumentationOptions, wrapper: Method): void {
39
+ if (!options.triggerKey) {
40
+ return;
41
+ }
42
+ try {
43
+ const plugin = getInstance();
44
+ plugin.registerTrigger(options.triggerKey, wrapper, options.triggerDescription);
45
+ markTriggerRegistered(wrapper);
46
+ } catch (error) {
47
+ if (error instanceof PluginNotInitializedError) {
48
+ queueTrigger(options.triggerKey, wrapper, options.triggerDescription);
49
+ } else {
50
+ throw error;
51
+ }
52
+ }
53
+ }
54
+
55
+ function ensureTriggerRegistration(plugin: TuringPulsePlugin, options: InstrumentationOptions, wrapper: Method): void {
56
+ if (!options.triggerKey || isTriggerRegistered(wrapper)) {
57
+ return;
58
+ }
59
+ plugin.registerTrigger(options.triggerKey, wrapper, options.triggerDescription);
60
+ markTriggerRegistered(wrapper);
61
+ }