xray-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,426 @@
1
+ # X-Ray SDK
2
+
3
+ **AI-powered observability for multi-step pipelines**
4
+
5
+ X-Ray is a lightweight SDK that automatically tracks pipeline executions and generates human-readable reasoning for each step using LLMs.
6
+
7
+ ## Features
8
+
9
+ ✨ **Automatic Step Tracking** - Track pipeline execution with simple `startStep()` / `endStep()` calls
10
+ 🤖 **AI-Powered Reasoning** - Generate natural language explanations for each step using OpenAI
11
+ 💾 **Flexible Storage** - In-memory or database-backed (Prisma + PostgreSQL)
12
+ ⚡ **Async Processing** - Non-blocking reasoning generation with retry logic
13
+ 🔄 **On-Demand Generation** - Only generate reasoning when you need it (cost savings)
14
+ 📊 **Job Queue** - Built-in queue for managing concurrent LLM calls
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install xray-sdk p-queue
20
+ ```
21
+
22
+ ### Optional Dependencies
23
+
24
+ ```bash
25
+ # For database storage
26
+ npm install @prisma/client
27
+
28
+ # For OpenAI reasoning
29
+ npm install openai
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### Basic Usage (In-Memory)
35
+
36
+ ```typescript
37
+ import { XRay, MemoryStorage } from 'xray-sdk'
38
+
39
+ const storage = new MemoryStorage()
40
+ const xray = new XRay('my-execution-1', { projectId: 'demo' }, storage)
41
+
42
+ // Track a step
43
+ xray.startStep('fetch_data', { query: 'shoes' })
44
+ const data = await fetchData('shoes')
45
+ xray.endStep('fetch_data', { results: data.length })
46
+
47
+ // End execution
48
+ const execution = xray.end({ success: true })
49
+ await xray.save()
50
+
51
+ console.log('Execution saved:', execution.executionId)
52
+ ```
53
+
54
+ ### With Database Storage (Prisma)
55
+
56
+ ```typescript
57
+ import { XRay, DatabaseStorage } from '@xray/sdk'
58
+ import { PrismaClient } from '@prisma/client'
59
+
60
+ const prisma = new PrismaClient()
61
+ const storage = new DatabaseStorage(prisma)
62
+
63
+ const xray = new XRay('my-execution-2', { projectId: 'demo' }, storage)
64
+
65
+ // Track steps
66
+ xray.startStep('step1', { input: 'data' })
67
+ xray.endStep('step1', { output: 'result' })
68
+
69
+ // Save to database
70
+ const execution = xray.end({ success: true })
71
+ await xray.save()
72
+ ```
73
+
74
+ ### With AI Reasoning (OpenAI)
75
+
76
+ ```typescript
77
+ import {
78
+ XRay,
79
+ DatabaseStorage,
80
+ ReasoningQueue,
81
+ createOpenAIGenerator
82
+ } from '@xray/sdk'
83
+ import { PrismaClient } from '@prisma/client'
84
+ import OpenAI from 'openai'
85
+
86
+ const prisma = new PrismaClient()
87
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
88
+
89
+ // Setup storage and reasoning
90
+ const storage = new DatabaseStorage(prisma)
91
+ const generator = createOpenAIGenerator(openai)
92
+ const queue = new ReasoningQueue(
93
+ storage,
94
+ generator,
95
+ { concurrency: 3, debug: true },
96
+ prisma
97
+ )
98
+
99
+ // Run pipeline
100
+ const xray = new XRay('my-execution-3', { projectId: 'demo' }, storage)
101
+
102
+ xray.startStep('search', { query: 'laptops' })
103
+ const results = await search('laptops')
104
+ xray.endStep('search', { count: results.length })
105
+
106
+ xray.startStep('filter', { threshold: 4.5 })
107
+ const filtered = results.filter(r => r.rating >= 4.5)
108
+ xray.endStep('filter', { remaining: filtered.length })
109
+
110
+ // Save execution (without reasoning)
111
+ const execution = xray.end({ success: true })
112
+ await xray.save()
113
+
114
+ // Enqueue reasoning generation (async)
115
+ await xray.enqueueReasoning(queue)
116
+
117
+ console.log('✅ Execution saved, reasoning generating in background')
118
+ ```
119
+
120
+ ### On-Demand Reasoning (Recommended)
121
+
122
+ Instead of auto-generating reasoning, trigger it only when viewing an execution:
123
+
124
+ ```typescript
125
+ // Pipeline API - just save execution
126
+ const execution = xray.end({ success: true })
127
+ await xray.save()
128
+ return { executionId: execution.executionId } // Returns instantly
129
+
130
+ // Later, when user views execution detail page
131
+ await queue.processExecution(executionId) // Generate reasoning now
132
+ ```
133
+
134
+ This approach:
135
+ - ✅ API responds instantly (~150ms)
136
+ - ✅ Saves LLM costs (only generate for executions users actually view)
137
+ - ✅ Better user experience
138
+
139
+ ## API Reference
140
+
141
+ ### XRay
142
+
143
+ ```typescript
144
+ class XRay {
145
+ constructor(executionId: string, metadata?: Record<string, any>, storage?: StorageProvider)
146
+
147
+ // V1 API (backward compatible)
148
+ logStep(step: { name: string, input: any, output: any, metadata?: any }): void
149
+
150
+ // V2 API (recommended)
151
+ startStep(name: string, input: any, metadata?: any): void
152
+ endStep(name: string, output: any): void
153
+ errorStep(name: string, error: Error): void
154
+
155
+ end(finalOutcome: any): Execution
156
+ save(): Promise<void>
157
+ enqueueReasoning(queue: ReasoningQueue): Promise<void>
158
+ getExecution(): Execution
159
+ }
160
+ ```
161
+
162
+ ### StorageProvider
163
+
164
+ ```typescript
165
+ interface StorageProvider {
166
+ saveExecution(execution: Execution): Promise<void>
167
+ getExecutionById(executionId: string): Promise<Execution | undefined>
168
+ getAllExecutions(): Promise<Execution[]>
169
+ updateStepReasoning(executionId: string, stepName: string, reasoning: string): Promise<void>
170
+ }
171
+ ```
172
+
173
+ **Implementations:**
174
+ - `MemoryStorage` - In-memory storage (testing)
175
+ - `DatabaseStorage` - Prisma + PostgreSQL (production)
176
+
177
+ ### ReasoningQueue
178
+
179
+ ```typescript
180
+ class ReasoningQueue {
181
+ constructor(
182
+ storage: StorageProvider,
183
+ generator: ReasoningGenerator,
184
+ config?: Partial<ReasoningConfig>,
185
+ prismaClient?: any
186
+ )
187
+
188
+ enqueue(executionId: string, stepName: string): Promise<string>
189
+ enqueueExecution(executionId: string): Promise<string[]>
190
+ processExecution(executionId: string): Promise<void>
191
+ getStats(): QueueStats
192
+ getJob(jobId: string): ReasoningJob | undefined
193
+ }
194
+ ```
195
+
196
+ ### Reasoning Generators
197
+
198
+ ```typescript
199
+ // OpenAI-powered reasoning
200
+ const generator = createOpenAIGenerator(openaiClient)
201
+
202
+ // Simple reasoning (no LLM)
203
+ const generator = createSimpleGenerator()
204
+
205
+ // Custom reasoning
206
+ const generator: ReasoningGenerator = async (step: Step) => {
207
+ return `My custom reasoning for ${step.name}`
208
+ }
209
+ ```
210
+
211
+ ## Configuration
212
+
213
+ ### Reasoning Config
214
+
215
+ ```typescript
216
+ interface ReasoningConfig {
217
+ concurrency: number // Parallel LLM calls (default: 3)
218
+ maxRetries: number // Max retries per job (default: 4)
219
+ retryDelays: number[] // Backoff delays in ms (default: [1000, 2000, 4000, 8000])
220
+ debug: boolean // Enable logging (default: false)
221
+ }
222
+
223
+ const config = createReasoningConfig({
224
+ concurrency: 5,
225
+ maxRetries: 3,
226
+ debug: true
227
+ })
228
+
229
+ const queue = new ReasoningQueue(storage, generator, config)
230
+ ```
231
+
232
+ ## Prisma Schema
233
+
234
+ If using `DatabaseStorage`, add this to your Prisma schema:
235
+
236
+ ```prisma
237
+ model Execution {
238
+ id String @id @default(cuid())
239
+ executionId String @unique
240
+ projectId String @default("default")
241
+ metadata Json?
242
+ finalOutcome Json?
243
+ startedAt DateTime @default(now())
244
+ completedAt DateTime?
245
+ steps Step[]
246
+ reasoningJobs ReasoningJob[]
247
+ }
248
+
249
+ model Step {
250
+ id String @id @default(cuid())
251
+ executionId String
252
+ execution Execution @relation(fields: [executionId], references: [id], onDelete: Cascade)
253
+ name String
254
+ input Json?
255
+ output Json?
256
+ error String?
257
+ durationMs Int?
258
+ reasoning String?
259
+ createdAt DateTime @default(now())
260
+ }
261
+
262
+ model ReasoningJob {
263
+ id String @id @default(cuid())
264
+ executionId String
265
+ execution Execution @relation(fields: [executionId], references: [id], onDelete: Cascade)
266
+ stepName String
267
+ status String
268
+ reasoning String?
269
+ error String?
270
+ attempts Int @default(0)
271
+ createdAt DateTime @default(now())
272
+ completedAt DateTime?
273
+
274
+ @@unique([executionId, stepName])
275
+ }
276
+ ```
277
+
278
+ Then run:
279
+ ```bash
280
+ npx prisma migrate dev --name add_xray
281
+ npx prisma generate
282
+ ```
283
+
284
+ ## Examples
285
+
286
+ ### Example 1: Simple Pipeline
287
+
288
+ ```typescript
289
+ import { XRay, MemoryStorage } from '@xray/sdk'
290
+
291
+ async function runPipeline() {
292
+ const storage = new MemoryStorage()
293
+ const xray = new XRay('exec-1', {}, storage)
294
+
295
+ xray.startStep('fetch', { url: 'https://api.example.com' })
296
+ const data = await fetch('https://api.example.com').then(r => r.json())
297
+ xray.endStep('fetch', { count: data.length })
298
+
299
+ xray.startStep('process', { data })
300
+ const processed = data.map(d => d.value * 2)
301
+ xray.endStep('process', { result: processed })
302
+
303
+ const execution = xray.end({ total: processed.length })
304
+ await xray.save()
305
+
306
+ return execution
307
+ }
308
+ ```
309
+
310
+ ### Example 2: With Error Handling
311
+
312
+ ```typescript
313
+ xray.startStep('risky_operation', { input: 'data' })
314
+ try {
315
+ const result = await riskyOperation()
316
+ xray.endStep('risky_operation', { result })
317
+ } catch (error) {
318
+ xray.errorStep('risky_operation', error as Error)
319
+ }
320
+ ```
321
+
322
+ ### Example 3: Custom Storage
323
+
324
+ ```typescript
325
+ import { StorageProvider, Execution } from '@xray/sdk'
326
+
327
+ class S3Storage implements StorageProvider {
328
+ async saveExecution(execution: Execution): Promise<void> {
329
+ // Upload to S3
330
+ await s3.putObject({
331
+ Bucket: 'my-bucket',
332
+ Key: `executions/${execution.executionId}.json`,
333
+ Body: JSON.stringify(execution)
334
+ })
335
+ }
336
+
337
+ async getExecutionById(id: string): Promise<Execution | undefined> {
338
+ // Download from S3
339
+ const obj = await s3.getObject({
340
+ Bucket: 'my-bucket',
341
+ Key: `executions/${id}.json`
342
+ })
343
+ return JSON.parse(obj.Body.toString())
344
+ }
345
+
346
+ // ... implement other methods
347
+ }
348
+ ```
349
+
350
+ ## Best Practices
351
+
352
+ ### 1. Use On-Demand Reasoning
353
+
354
+ Don't generate reasoning on every pipeline run - only when users view executions:
355
+
356
+ ```typescript
357
+ // ❌ Bad: Auto-generate reasoning (slow, expensive)
358
+ await xray.save()
359
+ await xray.enqueueReasoning(queue) // Blocks API response
360
+ return { executionId }
361
+
362
+ // ✅ Good: Generate on-demand (fast, cost-effective)
363
+ await xray.save()
364
+ return { executionId } // Returns instantly
365
+
366
+ // Later, when viewing execution:
367
+ await queue.processExecution(executionId)
368
+ ```
369
+
370
+ ### 2. Use Database Storage in Production
371
+
372
+ ```typescript
373
+ // ❌ Bad: In-memory (data lost on restart)
374
+ const storage = new MemoryStorage()
375
+
376
+ // ✅ Good: Database-backed (persistent)
377
+ const storage = new DatabaseStorage(prisma)
378
+ ```
379
+
380
+ ### 3. Configure Concurrency
381
+
382
+ ```typescript
383
+ // Balance API costs vs speed
384
+ const queue = new ReasoningQueue(storage, generator, {
385
+ concurrency: 3, // 3 parallel LLM calls
386
+ maxRetries: 4, // Retry failed jobs
387
+ debug: true // Enable logging
388
+ })
389
+ ```
390
+
391
+ ### 4. Handle Errors Gracefully
392
+
393
+ ```typescript
394
+ xray.startStep('step', { input })
395
+ try {
396
+ const result = await operation()
397
+ xray.endStep('step', { result })
398
+ } catch (error) {
399
+ xray.errorStep('step', error as Error)
400
+ // Continue pipeline or throw
401
+ }
402
+ ```
403
+
404
+ ## TypeScript
405
+
406
+ Full TypeScript support with type definitions:
407
+
408
+ ```typescript
409
+ import { Execution, Step, ReasoningJob, QueueStats } from '@xray/sdk'
410
+
411
+ const execution: Execution = xray.getExecution()
412
+ const stats: QueueStats = queue.getStats()
413
+ ```
414
+
415
+ ## License
416
+
417
+ MIT
418
+
419
+ ## Contributing
420
+
421
+ Contributions welcome! Please open an issue or PR.
422
+
423
+ ## Support
424
+
425
+ - GitHub Issues: https://github.com/yourusername/xray-sdk/issues
426
+ - Documentation: https://xray-sdk.dev
package/dist/XRay.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Execution, StorageProvider } from "./types";
2
+ import { ReasoningQueue } from "./reasoning/queue";
3
+ export declare class XRay {
4
+ private execution;
5
+ private activeSteps;
6
+ private pendingReasoningSteps;
7
+ private storage?;
8
+ constructor(executionId: string, metadata?: Record<string, any>, storage?: StorageProvider);
9
+ /** ✅ BACKWARD-COMPATIBLE (v1) – no auto reasoning */
10
+ logStep(step: {
11
+ name: string;
12
+ input: any;
13
+ output: any;
14
+ metadata?: Record<string, any>;
15
+ }): void;
16
+ /** Start a step (v2) */
17
+ startStep(name: string, input: any, metadata?: Record<string, any>): void;
18
+ /** Finish a step (v3) – stores without reasoning (populated asynchronously) */
19
+ endStep(name: string, output: any): void;
20
+ /** Capture error inside a step (v2) – stores without reasoning (populated asynchronously) */
21
+ errorStep(name: string, error: Error): void;
22
+ /** End execution - returns execution without enqueueing reasoning */
23
+ end(finalOutcome: any): Execution;
24
+ /** Save execution to storage (if configured) */
25
+ save(): Promise<void>;
26
+ /** Enqueue reasoning jobs - call AFTER saveExecution() */
27
+ enqueueReasoning(queue: ReasoningQueue): Promise<void>;
28
+ /** Get the current execution */
29
+ getExecution(): Execution;
30
+ }
package/dist/XRay.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XRay = void 0;
4
+ class XRay {
5
+ constructor(executionId, metadata, storage) {
6
+ this.activeSteps = new Map();
7
+ this.pendingReasoningSteps = [];
8
+ this.execution = {
9
+ executionId,
10
+ startedAt: new Date().toISOString(),
11
+ steps: [],
12
+ metadata
13
+ };
14
+ this.storage = storage;
15
+ }
16
+ /** ✅ BACKWARD-COMPATIBLE (v1) – no auto reasoning */
17
+ logStep(step) {
18
+ this.execution.steps.push({
19
+ name: step.name,
20
+ input: step.input,
21
+ output: step.output,
22
+ timestamp: new Date().toISOString(),
23
+ metadata: step.metadata
24
+ // reasoning can be added manually via metadata.reasoning if needed
25
+ });
26
+ }
27
+ /** Start a step (v2) */
28
+ startStep(name, input, metadata) {
29
+ const step = {
30
+ name,
31
+ input,
32
+ timestamp: new Date().toISOString(),
33
+ startedAt: new Date().toISOString(),
34
+ metadata
35
+ };
36
+ this.activeSteps.set(name, step);
37
+ }
38
+ /** Finish a step (v3) – stores without reasoning (populated asynchronously) */
39
+ endStep(name, output) {
40
+ const step = this.activeSteps.get(name);
41
+ if (!step)
42
+ return;
43
+ step.output = output;
44
+ step.endedAt = new Date().toISOString();
45
+ step.durationMs =
46
+ new Date(step.endedAt).getTime() -
47
+ new Date(step.startedAt).getTime();
48
+ // Store without reasoning (will be populated asynchronously)
49
+ step.reasoning = undefined;
50
+ this.execution.steps.push(step);
51
+ this.activeSteps.delete(name);
52
+ // Track step for later processing
53
+ this.pendingReasoningSteps.push(step.name);
54
+ }
55
+ /** Capture error inside a step (v2) – stores without reasoning (populated asynchronously) */
56
+ errorStep(name, error) {
57
+ const step = this.activeSteps.get(name);
58
+ if (!step)
59
+ return;
60
+ step.error = error.message;
61
+ step.endedAt = new Date().toISOString();
62
+ step.durationMs =
63
+ new Date(step.endedAt).getTime() -
64
+ new Date(step.startedAt).getTime();
65
+ // Store without reasoning (will be populated asynchronously)
66
+ step.reasoning = undefined;
67
+ this.execution.steps.push(step);
68
+ this.activeSteps.delete(name);
69
+ // Track step for later processing
70
+ this.pendingReasoningSteps.push(step.name);
71
+ }
72
+ /** End execution - returns execution without enqueueing reasoning */
73
+ end(finalOutcome) {
74
+ this.execution.endedAt = new Date().toISOString();
75
+ this.execution.finalOutcome = finalOutcome;
76
+ return this.execution;
77
+ }
78
+ /** Save execution to storage (if configured) */
79
+ async save() {
80
+ if (this.storage) {
81
+ await this.storage.saveExecution(this.execution);
82
+ }
83
+ }
84
+ /** Enqueue reasoning jobs - call AFTER saveExecution() */
85
+ async enqueueReasoning(queue) {
86
+ for (const stepName of this.pendingReasoningSteps) {
87
+ await queue.enqueue(this.execution.executionId, stepName);
88
+ }
89
+ }
90
+ /** Get the current execution */
91
+ getExecution() {
92
+ return this.execution;
93
+ }
94
+ }
95
+ exports.XRay = XRay;
@@ -0,0 +1,8 @@
1
+ export { XRay } from './XRay';
2
+ export { DatabaseStorage } from './storage/DatabaseStorage';
3
+ export { MemoryStorage } from './storage/MemoryStorage';
4
+ export { ReasoningQueue } from './reasoning/queue';
5
+ export { createOpenAIGenerator, createSimpleGenerator } from './reasoning/generator';
6
+ export { createReasoningConfig, DEFAULT_REASONING_CONFIG } from './reasoning/config';
7
+ export type { Execution, Step, ReasoningJob, QueueStats, XRayConfig, ReasoningConfig, StorageProvider } from './types';
8
+ export type { ReasoningGenerator } from './reasoning/generator';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ // Main entry point for X-Ray SDK
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.DEFAULT_REASONING_CONFIG = exports.createReasoningConfig = exports.createSimpleGenerator = exports.createOpenAIGenerator = exports.ReasoningQueue = exports.MemoryStorage = exports.DatabaseStorage = exports.XRay = void 0;
5
+ // Core classes
6
+ var XRay_1 = require("./XRay");
7
+ Object.defineProperty(exports, "XRay", { enumerable: true, get: function () { return XRay_1.XRay; } });
8
+ // Storage implementations
9
+ var DatabaseStorage_1 = require("./storage/DatabaseStorage");
10
+ Object.defineProperty(exports, "DatabaseStorage", { enumerable: true, get: function () { return DatabaseStorage_1.DatabaseStorage; } });
11
+ var MemoryStorage_1 = require("./storage/MemoryStorage");
12
+ Object.defineProperty(exports, "MemoryStorage", { enumerable: true, get: function () { return MemoryStorage_1.MemoryStorage; } });
13
+ // Reasoning
14
+ var queue_1 = require("./reasoning/queue");
15
+ Object.defineProperty(exports, "ReasoningQueue", { enumerable: true, get: function () { return queue_1.ReasoningQueue; } });
16
+ var generator_1 = require("./reasoning/generator");
17
+ Object.defineProperty(exports, "createOpenAIGenerator", { enumerable: true, get: function () { return generator_1.createOpenAIGenerator; } });
18
+ Object.defineProperty(exports, "createSimpleGenerator", { enumerable: true, get: function () { return generator_1.createSimpleGenerator; } });
19
+ var config_1 = require("./reasoning/config");
20
+ Object.defineProperty(exports, "createReasoningConfig", { enumerable: true, get: function () { return config_1.createReasoningConfig; } });
21
+ Object.defineProperty(exports, "DEFAULT_REASONING_CONFIG", { enumerable: true, get: function () { return config_1.DEFAULT_REASONING_CONFIG; } });
@@ -0,0 +1,8 @@
1
+ export interface ReasoningConfig {
2
+ concurrency: number;
3
+ maxRetries: number;
4
+ retryDelays: number[];
5
+ debug: boolean;
6
+ }
7
+ export declare const DEFAULT_REASONING_CONFIG: ReasoningConfig;
8
+ export declare function createReasoningConfig(overrides?: Partial<ReasoningConfig>): ReasoningConfig;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_REASONING_CONFIG = void 0;
4
+ exports.createReasoningConfig = createReasoningConfig;
5
+ exports.DEFAULT_REASONING_CONFIG = {
6
+ concurrency: 3,
7
+ maxRetries: 4,
8
+ retryDelays: [1000, 2000, 4000, 8000], // 1s, 2s, 4s, 8s
9
+ debug: false
10
+ };
11
+ function createReasoningConfig(overrides) {
12
+ return {
13
+ ...exports.DEFAULT_REASONING_CONFIG,
14
+ ...overrides
15
+ };
16
+ }
@@ -0,0 +1,12 @@
1
+ import { Step } from "../types";
2
+ export type ReasoningGenerator = (step: Step) => Promise<string>;
3
+ /**
4
+ * Default reasoning generator using OpenAI
5
+ * Users can provide their own OpenAI client instance
6
+ */
7
+ export declare function createOpenAIGenerator(openaiClient: any): ReasoningGenerator;
8
+ /**
9
+ * Simple reasoning generator (no LLM)
10
+ * Returns numeric summaries based on step data
11
+ */
12
+ export declare function createSimpleGenerator(): ReasoningGenerator;