@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,232 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { registerGovernanceHooks } from '../src/hooks/governance.js';
3
+ import { SessionTracker } from '../src/session-tracker.js';
4
+ import type { TuringPulseOpenClawConfig } from '../src/config.js';
5
+ import type { OpenClawPluginAPI } from '../src/types.js';
6
+
7
+ function makeConfig(overrides?: Partial<TuringPulseOpenClawConfig['governance']>): TuringPulseOpenClawConfig {
8
+ return {
9
+ apiKey: 'tp-test-key',
10
+ endpoint: 'https://api.turingpulse.ai',
11
+ governance: {
12
+ enabled: true,
13
+ failMode: 'open',
14
+ timeoutMs: 3000,
15
+ excludeTools: [],
16
+ scanOutboundMessages: false,
17
+ ...overrides,
18
+ },
19
+ telemetry: {
20
+ enabled: true,
21
+ batchSize: 20,
22
+ flushIntervalMs: 5000,
23
+ captureMessageContent: false,
24
+ captureToolParams: true,
25
+ redactPatterns: [],
26
+ },
27
+ metadata: {},
28
+ };
29
+ }
30
+
31
+ function makeMockApi() {
32
+ const handlers = new Map<string, (ctx: unknown) => Promise<unknown>>();
33
+ const api: OpenClawPluginAPI = {
34
+ config: {},
35
+ lifecycle: {
36
+ on: vi.fn((phase: string, handler: (ctx: unknown) => Promise<unknown>) => {
37
+ handlers.set(phase, handler);
38
+ }),
39
+ },
40
+ registerHook: vi.fn(),
41
+ registerCommand: vi.fn(),
42
+ };
43
+ return { api, handlers };
44
+ }
45
+
46
+ function makeMockClient(response?: { action: string; triggered: boolean; reason?: string; policyIds: string[] }) {
47
+ return {
48
+ policyCheck: vi.fn().mockResolvedValue(
49
+ response ?? { action: 'allow', triggered: false, policyIds: [] },
50
+ ),
51
+ };
52
+ }
53
+
54
+ describe('governance hooks', () => {
55
+ let sessionTracker: SessionTracker;
56
+
57
+ beforeEach(() => {
58
+ sessionTracker = new SessionTracker();
59
+ });
60
+
61
+ it('registers tool.pre hook when governance is enabled', () => {
62
+ const { api } = makeMockApi();
63
+ const client = makeMockClient();
64
+ const config = makeConfig();
65
+
66
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
67
+
68
+ expect(api.lifecycle.on).toHaveBeenCalledWith(
69
+ 'tool.pre',
70
+ expect.any(Function),
71
+ expect.objectContaining({ priority: 1000 }),
72
+ );
73
+ });
74
+
75
+ it('does not register hooks when governance is disabled', () => {
76
+ const { api } = makeMockApi();
77
+ const client = makeMockClient();
78
+ const config = makeConfig({ enabled: false });
79
+
80
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
81
+
82
+ expect(api.lifecycle.on).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('allows tool execution when policy returns allow', async () => {
86
+ const { api, handlers } = makeMockApi();
87
+ const client = makeMockClient({ action: 'allow', triggered: false, policyIds: [] });
88
+ const config = makeConfig();
89
+
90
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
91
+
92
+ const handler = handlers.get('tool.pre')!;
93
+ const result = await handler({
94
+ toolName: 'exec',
95
+ toolParams: { command: 'git status' },
96
+ sessionKey: 'sess-1',
97
+ });
98
+
99
+ expect(result).toBeUndefined();
100
+ expect(client.policyCheck).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ it('cancels tool execution when policy returns block', async () => {
104
+ const { api, handlers } = makeMockApi();
105
+ const client = makeMockClient({
106
+ action: 'block',
107
+ triggered: true,
108
+ reason: 'Dangerous command detected',
109
+ policyIds: ['policy-1'],
110
+ });
111
+ const config = makeConfig();
112
+
113
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
114
+
115
+ const handler = handlers.get('tool.pre')!;
116
+ const result = await handler({
117
+ toolName: 'exec',
118
+ toolParams: { command: 'rm -rf /' },
119
+ sessionKey: 'sess-1',
120
+ });
121
+
122
+ expect(result).toEqual({ cancel: true });
123
+ });
124
+
125
+ it('allows execution on flag (but logs it)', async () => {
126
+ const { api, handlers } = makeMockApi();
127
+ const client = makeMockClient({
128
+ action: 'flag',
129
+ triggered: true,
130
+ reason: 'Potentially risky operation',
131
+ policyIds: ['policy-2'],
132
+ });
133
+ const config = makeConfig();
134
+
135
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
136
+
137
+ const handler = handlers.get('tool.pre')!;
138
+ const result = await handler({
139
+ toolName: 'exec',
140
+ toolParams: { command: 'sudo apt update' },
141
+ sessionKey: 'sess-1',
142
+ });
143
+
144
+ expect(result).toBeUndefined();
145
+ });
146
+
147
+ it('skips excluded tools', async () => {
148
+ const { api, handlers } = makeMockApi();
149
+ const client = makeMockClient();
150
+ const config = makeConfig({ excludeTools: ['jq', 'cat'] });
151
+
152
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
153
+
154
+ const handler = handlers.get('tool.pre')!;
155
+ const result = await handler({
156
+ toolName: 'cat',
157
+ toolParams: { file: '/tmp/test.txt' },
158
+ });
159
+
160
+ expect(result).toBeUndefined();
161
+ expect(client.policyCheck).not.toHaveBeenCalled();
162
+ });
163
+
164
+ it('fail-open: allows execution when policyCheck throws', async () => {
165
+ const { api, handlers } = makeMockApi();
166
+ const client = makeMockClient();
167
+ client.policyCheck.mockRejectedValueOnce(new Error('network timeout'));
168
+ const config = makeConfig({ failMode: 'open' });
169
+
170
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
171
+
172
+ const handler = handlers.get('tool.pre')!;
173
+ const result = await handler({
174
+ toolName: 'exec',
175
+ toolParams: { command: 'ls' },
176
+ });
177
+
178
+ expect(result).toBeUndefined();
179
+ });
180
+
181
+ it('fail-closed: blocks execution when policyCheck throws', async () => {
182
+ const { api, handlers } = makeMockApi();
183
+ const client = makeMockClient();
184
+ client.policyCheck.mockRejectedValueOnce(new Error('network timeout'));
185
+ const config = makeConfig({ failMode: 'closed' });
186
+
187
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
188
+
189
+ const handler = handlers.get('tool.pre')!;
190
+ const result = await handler({
191
+ toolName: 'exec',
192
+ toolParams: { command: 'ls' },
193
+ });
194
+
195
+ expect(result).toEqual({ cancel: true });
196
+ });
197
+
198
+ it('includes session cost in policy check params', async () => {
199
+ const { api, handlers } = makeMockApi();
200
+ const client = makeMockClient();
201
+ const config = makeConfig();
202
+
203
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
204
+
205
+ sessionTracker.start('sess-1', 'trace-1', 'root-1');
206
+ sessionTracker.addMetrics('sess-1', { costUsd: 2.50 });
207
+
208
+ const handler = handlers.get('tool.pre')!;
209
+ await handler({
210
+ toolName: 'exec',
211
+ toolParams: { command: 'deploy.sh' },
212
+ sessionKey: 'sess-1',
213
+ });
214
+
215
+ const checkParams = client.policyCheck.mock.calls[0][0];
216
+ expect(checkParams.costUsd).toBe(2.50);
217
+ expect(checkParams.runId).toBe('trace-1');
218
+ });
219
+
220
+ it('registers message.pre hook when scanOutboundMessages is enabled', () => {
221
+ const { api } = makeMockApi();
222
+ const client = makeMockClient();
223
+ const config = makeConfig({ scanOutboundMessages: true });
224
+
225
+ registerGovernanceHooks(api, client as never, config, sessionTracker);
226
+
227
+ const phases = (api.lifecycle.on as ReturnType<typeof vi.fn>).mock.calls.map(
228
+ (call: unknown[]) => call[0],
229
+ );
230
+ expect(phases).toContain('message.pre');
231
+ });
232
+ });
@@ -0,0 +1,242 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildRootSpan, buildToolSpan, buildAgentReasoningSpan } from '../src/mapper.js';
3
+ import type { SessionState } from '../src/session-tracker.js';
4
+ import type { TuringPulseOpenClawConfig } from '../src/config.js';
5
+ import type { ToolPostContext, AgentPostContext } from '../src/types.js';
6
+
7
+ function makeConfig(overrides?: Partial<TuringPulseOpenClawConfig['telemetry']>): TuringPulseOpenClawConfig {
8
+ return {
9
+ apiKey: 'tp-test',
10
+ endpoint: 'https://api.turingpulse.ai',
11
+ governance: {
12
+ enabled: true,
13
+ failMode: 'open',
14
+ timeoutMs: 3000,
15
+ excludeTools: [],
16
+ scanOutboundMessages: false,
17
+ },
18
+ telemetry: {
19
+ enabled: true,
20
+ batchSize: 20,
21
+ flushIntervalMs: 5000,
22
+ captureMessageContent: false,
23
+ captureToolParams: true,
24
+ redactPatterns: [],
25
+ ...overrides,
26
+ },
27
+ metadata: { environment: 'test' },
28
+ };
29
+ }
30
+
31
+ function makeSession(overrides?: Partial<SessionState>): SessionState {
32
+ return {
33
+ traceId: 'trace-abc',
34
+ rootSpanId: 'root-123',
35
+ startedAt: Date.now() - 1000,
36
+ childEvents: [],
37
+ channelId: 'whatsapp',
38
+ agentId: 'agent-pi',
39
+ tokens: { prompt: 500, completion: 200 },
40
+ costUsd: 0.05,
41
+ toolCallCount: 3,
42
+ currentDepth: 0,
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ describe('buildRootSpan', () => {
48
+ it('creates a root workflow span with correct structure', () => {
49
+ const session = makeSession();
50
+ const config = makeConfig();
51
+
52
+ const event = buildRootSpan(session, config, {
53
+ status: 'success',
54
+ });
55
+
56
+ expect(event.runId).toBe('trace-abc');
57
+ expect(event.agentId).toBe('agent-pi');
58
+ expect(event.type).toBe('span');
59
+ expect(event.workflowName).toBe('agent-pi');
60
+ expect(event.payload.name).toBe('agent-pi.execute');
61
+ expect(event.payload.status).toBe('success');
62
+ expect(event.payload.tokens).toEqual({ prompt: 500, completion: 200 });
63
+ expect(event.payload.costUsd).toBe(0.05);
64
+ expect(event.payload.durationMs).toBeGreaterThan(0);
65
+
66
+ const meta = event.payload.metadata!;
67
+ expect(meta.framework).toBe('openclaw');
68
+ expect(meta.node_type).toBe('workflow');
69
+ expect(meta.trace_id).toBe('trace-abc');
70
+ expect(meta.span_id).toBe('root-123');
71
+ expect(meta.channel).toBe('whatsapp');
72
+ expect(meta.environment).toBe('test');
73
+ });
74
+
75
+ it('includes error in metadata when status is error', () => {
76
+ const session = makeSession();
77
+ const config = makeConfig();
78
+
79
+ const event = buildRootSpan(session, config, {
80
+ status: 'error',
81
+ error: 'Agent crashed',
82
+ });
83
+
84
+ expect(event.payload.status).toBe('error');
85
+ expect(event.payload.metadata!.error_message).toBe('Agent crashed');
86
+ });
87
+
88
+ it('omits message content when captureMessageContent is false', () => {
89
+ const session = makeSession();
90
+ const config = makeConfig({ captureMessageContent: false });
91
+
92
+ const event = buildRootSpan(session, config, {
93
+ inputText: 'secret user message',
94
+ outputText: 'secret response',
95
+ status: 'success',
96
+ });
97
+
98
+ expect(event.payload.metadata!.input).toBeUndefined();
99
+ expect(event.payload.metadata!.output).toBeUndefined();
100
+ });
101
+
102
+ it('includes message content when captureMessageContent is true', () => {
103
+ const session = makeSession();
104
+ const config = makeConfig({ captureMessageContent: true });
105
+
106
+ const event = buildRootSpan(session, config, {
107
+ inputText: 'hello world',
108
+ outputText: 'hi there',
109
+ status: 'success',
110
+ });
111
+
112
+ expect(event.payload.metadata!.input).toBe('hello world');
113
+ expect(event.payload.metadata!.output).toBe('hi there');
114
+ });
115
+
116
+ it('falls back to default agent name when agentId is missing', () => {
117
+ const session = makeSession({ agentId: undefined });
118
+ const config = makeConfig();
119
+
120
+ const event = buildRootSpan(session, config, { status: 'success' });
121
+
122
+ expect(event.agentId).toBe('openclaw-agent');
123
+ expect(event.payload.name).toBe('openclaw-agent.execute');
124
+ });
125
+ });
126
+
127
+ describe('buildToolSpan', () => {
128
+ it('creates a tool child span with correct structure', () => {
129
+ const session = makeSession();
130
+ const config = makeConfig();
131
+ const ctx: ToolPostContext = {
132
+ toolName: 'exec',
133
+ toolParams: { command: 'git status' },
134
+ result: 'On branch main',
135
+ durationMs: 250,
136
+ sessionKey: 'sess-1',
137
+ };
138
+
139
+ const event = buildToolSpan(ctx, session, 'span-tool-1', config);
140
+
141
+ expect(event.runId).toBe('trace-abc');
142
+ expect(event.agentId).toBe('tool_exec');
143
+ expect(event.type).toBe('span');
144
+ expect(event.payload.name).toBe('agent-pi.tool_exec');
145
+ expect(event.payload.status).toBe('success');
146
+ expect(event.payload.durationMs).toBe(250);
147
+
148
+ const meta = event.payload.metadata!;
149
+ expect(meta.span_id).toBe('span-tool-1');
150
+ expect(meta.parent_span_id).toBe('root-123');
151
+ expect(meta.trace_id).toBe('trace-abc');
152
+ expect(meta.node_type).toBe('tool');
153
+ expect(meta.tool_name).toBe('exec');
154
+ expect(meta.input).toContain('git status');
155
+
156
+ expect(event.payload.toolCalls).toHaveLength(1);
157
+ expect(event.payload.toolCalls![0].toolName).toBe('exec');
158
+ expect(event.payload.toolCalls![0].success).toBe(true);
159
+ });
160
+
161
+ it('marks tool span as error when ctx.error is present', () => {
162
+ const session = makeSession();
163
+ const config = makeConfig();
164
+ const ctx: ToolPostContext = {
165
+ toolName: 'exec',
166
+ toolParams: { command: 'bad-command' },
167
+ result: null,
168
+ error: new Error('command not found'),
169
+ sessionKey: 'sess-1',
170
+ };
171
+
172
+ const event = buildToolSpan(ctx, session, 'span-err', config);
173
+
174
+ expect(event.payload.status).toBe('error');
175
+ expect(event.payload.metadata!.error_message).toBe('command not found');
176
+ expect(event.payload.toolCalls![0].success).toBe(false);
177
+ });
178
+
179
+ it('omits tool params when captureToolParams is false', () => {
180
+ const session = makeSession();
181
+ const config = makeConfig({ captureToolParams: false });
182
+ const ctx: ToolPostContext = {
183
+ toolName: 'exec',
184
+ toolParams: { command: 'secret-command' },
185
+ result: 'done',
186
+ sessionKey: 'sess-1',
187
+ };
188
+
189
+ const event = buildToolSpan(ctx, session, 'span-1', config);
190
+
191
+ expect(event.payload.metadata!.input).toBeUndefined();
192
+ expect(event.payload.toolCalls![0].toolArgs).toEqual({});
193
+ });
194
+ });
195
+
196
+ describe('buildAgentReasoningSpan', () => {
197
+ it('creates an LLM reasoning span with token counts', () => {
198
+ const session = makeSession();
199
+ const config = makeConfig();
200
+ const ctx: AgentPostContext = {
201
+ sessionKey: 'sess-1',
202
+ model: 'anthropic/claude-sonnet-4-5',
203
+ usage: { promptTokens: 400, completionTokens: 150 },
204
+ durationMs: 2000,
205
+ };
206
+
207
+ const event = buildAgentReasoningSpan(ctx, session, 'span-llm-1', config);
208
+
209
+ expect(event.payload.name).toBe('agent-pi.agent_reasoning');
210
+ expect(event.payload.tokens).toEqual({ prompt: 400, completion: 150 });
211
+ expect(event.payload.durationMs).toBe(2000);
212
+
213
+ const meta = event.payload.metadata!;
214
+ expect(meta.node_type).toBe('llm');
215
+ expect(meta.model).toBe('anthropic/claude-sonnet-4-5');
216
+ expect(meta.provider).toBe('anthropic');
217
+ });
218
+
219
+ it('infers provider from model string', () => {
220
+ const session = makeSession();
221
+ const config = makeConfig();
222
+
223
+ const cases = [
224
+ { model: 'openai/gpt-4o', expected: 'openai' },
225
+ { model: 'anthropic/claude-sonnet-4-5', expected: 'anthropic' },
226
+ { model: 'google/gemini-2.5-pro', expected: 'google' },
227
+ { model: 'gpt-4o', expected: 'openai' },
228
+ { model: 'claude-sonnet-4-5', expected: 'anthropic' },
229
+ { model: undefined, expected: 'unknown' },
230
+ ];
231
+
232
+ for (const { model, expected } of cases) {
233
+ const event = buildAgentReasoningSpan(
234
+ { sessionKey: 'sess-1', model } as AgentPostContext,
235
+ session,
236
+ 'span-1',
237
+ config,
238
+ );
239
+ expect(event.payload.metadata!.provider).toBe(expected);
240
+ }
241
+ });
242
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SessionTracker } from '../src/session-tracker.js';
3
+
4
+ describe('SessionTracker', () => {
5
+ it('starts and retrieves a session', () => {
6
+ const tracker = new SessionTracker();
7
+ const session = tracker.start('sess-1', 'trace-1', 'root-1', 'whatsapp', 'agent-pi');
8
+
9
+ expect(session.traceId).toBe('trace-1');
10
+ expect(session.rootSpanId).toBe('root-1');
11
+ expect(session.channelId).toBe('whatsapp');
12
+ expect(session.agentId).toBe('agent-pi');
13
+ expect(session.tokens).toEqual({ prompt: 0, completion: 0 });
14
+ expect(session.costUsd).toBe(0);
15
+ expect(session.toolCallCount).toBe(0);
16
+ expect(session.childEvents).toEqual([]);
17
+
18
+ const retrieved = tracker.get('sess-1');
19
+ expect(retrieved).toBe(session);
20
+ });
21
+
22
+ it('returns undefined for unknown session', () => {
23
+ const tracker = new SessionTracker();
24
+ expect(tracker.get('unknown')).toBeUndefined();
25
+ });
26
+
27
+ it('accumulates metrics', () => {
28
+ const tracker = new SessionTracker();
29
+ tracker.start('sess-1', 'trace-1', 'root-1');
30
+
31
+ tracker.addMetrics('sess-1', { promptTokens: 100, completionTokens: 50, costUsd: 0.01, toolCalls: 1 });
32
+ tracker.addMetrics('sess-1', { promptTokens: 200, completionTokens: 80, costUsd: 0.02, toolCalls: 2 });
33
+
34
+ const session = tracker.get('sess-1')!;
35
+ expect(session.tokens.prompt).toBe(300);
36
+ expect(session.tokens.completion).toBe(130);
37
+ expect(session.costUsd).toBeCloseTo(0.03);
38
+ expect(session.toolCallCount).toBe(3);
39
+ });
40
+
41
+ it('ignores metrics for unknown session', () => {
42
+ const tracker = new SessionTracker();
43
+ expect(() => tracker.addMetrics('unknown', { promptTokens: 100 })).not.toThrow();
44
+ });
45
+
46
+ it('adds child events', () => {
47
+ const tracker = new SessionTracker();
48
+ tracker.start('sess-1', 'trace-1', 'root-1');
49
+
50
+ const event = {
51
+ runId: 'trace-1',
52
+ agentId: 'tool_exec',
53
+ timestamp: new Date().toISOString(),
54
+ type: 'span' as const,
55
+ payload: { name: 'test.tool_exec' },
56
+ };
57
+
58
+ tracker.addChildEvent('sess-1', event);
59
+ expect(tracker.get('sess-1')!.childEvents).toHaveLength(1);
60
+ });
61
+
62
+ it('finishes a session and returns accumulated state', () => {
63
+ const tracker = new SessionTracker();
64
+ tracker.start('sess-1', 'trace-1', 'root-1');
65
+ tracker.addMetrics('sess-1', { toolCalls: 3 });
66
+
67
+ const finished = tracker.finish('sess-1');
68
+ expect(finished).toBeDefined();
69
+ expect(finished!.toolCallCount).toBe(3);
70
+
71
+ expect(tracker.get('sess-1')).toBeUndefined();
72
+ expect(tracker.size).toBe(0);
73
+ });
74
+
75
+ it('returns undefined when finishing unknown session', () => {
76
+ const tracker = new SessionTracker();
77
+ expect(tracker.finish('unknown')).toBeUndefined();
78
+ });
79
+
80
+ it('enforces capacity limit via LRU eviction', () => {
81
+ const tracker = new SessionTracker(3);
82
+
83
+ tracker.start('sess-1', 't1', 'r1');
84
+ tracker.start('sess-2', 't2', 'r2');
85
+ tracker.start('sess-3', 't3', 'r3');
86
+
87
+ expect(tracker.size).toBe(3);
88
+
89
+ tracker.start('sess-4', 't4', 'r4');
90
+
91
+ expect(tracker.size).toBe(3);
92
+ expect(tracker.get('sess-1')).toBeUndefined();
93
+ expect(tracker.get('sess-4')).toBeDefined();
94
+ });
95
+
96
+ it('LRU eviction respects access order', () => {
97
+ const tracker = new SessionTracker(3);
98
+
99
+ tracker.start('sess-1', 't1', 'r1');
100
+ tracker.start('sess-2', 't2', 'r2');
101
+ tracker.start('sess-3', 't3', 'r3');
102
+
103
+ tracker.get('sess-1');
104
+
105
+ tracker.start('sess-4', 't4', 'r4');
106
+
107
+ expect(tracker.get('sess-1')).toBeDefined();
108
+ expect(tracker.get('sess-2')).toBeUndefined();
109
+ });
110
+
111
+ it('reports size correctly', () => {
112
+ const tracker = new SessionTracker();
113
+ expect(tracker.size).toBe(0);
114
+
115
+ tracker.start('sess-1', 't1', 'r1');
116
+ expect(tracker.size).toBe(1);
117
+
118
+ tracker.start('sess-2', 't2', 'r2');
119
+ expect(tracker.size).toBe(2);
120
+
121
+ tracker.finish('sess-1');
122
+ expect(tracker.size).toBe(1);
123
+ });
124
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "NodeNext",
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
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src"]
18
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ environment: 'node',
7
+ },
8
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@turingpulse/sdk-vercel-ai",
3
+ "version": "1.0.1",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "TuringPulse SDK integration for Vercel AI SDK",
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
+ "ai": ">=6.0.0",
14
+ "typescript": ">=5.0.0"
15
+ }
16
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * TuringPulse SDK integration for Vercel AI SDK.
3
+ */
4
+
5
+ export { instrumentVercelAI } from './wrapper';
@@ -0,0 +1,49 @@
1
+ import { withInstrumentation, GovernanceDirective, currentContext } from '@turingpulse/sdk';
2
+
3
+ interface VercelAIInstrumentOptions {
4
+ name: string;
5
+ governance?: GovernanceDirective;
6
+ model?: string;
7
+ provider?: string;
8
+ }
9
+
10
+ /**
11
+ * Wrap Vercel AI SDK's generateText/streamText for TuringPulse observability.
12
+ */
13
+ export function instrumentVercelAI(
14
+ generateFn: (...args: unknown[]) => Promise<unknown>,
15
+ options: VercelAIInstrumentOptions,
16
+ ): (...args: unknown[]) => Promise<unknown> {
17
+ const { name, governance, model, provider = 'openai' } = options;
18
+
19
+ const wrappedFn = withInstrumentation(
20
+ async function vercelAIGenerate(...args: unknown[]): Promise<unknown> {
21
+ const result = await generateFn(...args);
22
+
23
+ const ctx = currentContext();
24
+ if (ctx) {
25
+ ctx.framework = 'vercel-ai';
26
+ ctx.nodeType = 'llm';
27
+
28
+ const resp = result as Record<string, unknown>;
29
+ const usage = resp?.usage as { promptTokens?: number; completionTokens?: number } | undefined;
30
+ if (usage) {
31
+ ctx.setTokens(usage.promptTokens ?? 0, usage.completionTokens ?? 0);
32
+ }
33
+ if (model) {
34
+ ctx.setModel(model, provider);
35
+ }
36
+
37
+ const firstArg = args[0] as Record<string, unknown> | undefined;
38
+ const inputStr = (firstArg?.prompt as string) ?? JSON.stringify(firstArg);
39
+ const outputStr = (resp?.text as string) ?? JSON.stringify(result);
40
+ ctx.setIO(inputStr, outputStr);
41
+ }
42
+
43
+ return result;
44
+ },
45
+ { name, governance },
46
+ );
47
+
48
+ return wrappedFn as (...args: unknown[]) => Promise<unknown>;
49
+ }