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.
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOpenAIGenerator = createOpenAIGenerator;
4
+ exports.createSimpleGenerator = createSimpleGenerator;
5
+ /**
6
+ * Default reasoning generator using OpenAI
7
+ * Users can provide their own OpenAI client instance
8
+ */
9
+ function createOpenAIGenerator(openaiClient) {
10
+ return async (step) => {
11
+ console.log(`[XRay LLM] 🚀 Generating reasoning for step: ${step.name}`);
12
+ // Try numeric reasoning first (free, fast)
13
+ const numericReasoning = generateNumericReasoning(step);
14
+ if (numericReasoning) {
15
+ console.log(`[XRay LLM] ✓ Using numeric reasoning: "${numericReasoning}"`);
16
+ return numericReasoning;
17
+ }
18
+ // Handle errors
19
+ if (step.error) {
20
+ return `❌ ${step.name} failed: ${step.error}`;
21
+ }
22
+ try {
23
+ const prompt = `You are an AI pipeline observability expert. Generate a concise 1-2 sentence explanation for this step.
24
+
25
+ Input: ${JSON.stringify(step.input ?? {})}
26
+
27
+ Output: ${JSON.stringify(step.output ?? {})}
28
+
29
+ Rules:
30
+ - Be specific and mention counts, thresholds, or key decisions
31
+ - Use neutral, technical language
32
+ - Do NOT restate raw data verbatim
33
+ - ONLY return the reasoning text, no JSON formatting
34
+
35
+ Reasoning:`;
36
+ console.log(`[XRay LLM] 📤 Sending request to OpenAI API...`);
37
+ const completion = await openaiClient.chat.completions.create({
38
+ model: "gpt-4o-mini",
39
+ messages: [{ role: "user", content: prompt }],
40
+ max_tokens: 150,
41
+ temperature: 0.1,
42
+ });
43
+ console.log(`[XRay LLM] ✓ Received response from OpenAI API`);
44
+ const rawResponse = completion.choices[0]?.message?.content?.trim() || "Step processed";
45
+ console.log(`[XRay LLM] 📝 Raw response (${rawResponse.length} chars): "${rawResponse}"`);
46
+ // Clean up the response
47
+ let reasoning = rawResponse
48
+ .replace(/^Reasoning:\s*/i, '')
49
+ .replace(/```json\s*/g, '')
50
+ .replace(/```\s*/g, '')
51
+ .trim();
52
+ // If response looks like truncated JSON, return fallback
53
+ if (reasoning.startsWith('{') && !reasoning.endsWith('}')) {
54
+ console.log(`[XRay LLM] ⚠️ Detected truncated JSON response, using fallback`);
55
+ const fallback = generateNumericReasoning(step) || `Processed ${step.name}`;
56
+ console.log(`[XRay LLM] ✓ Using fallback: "${fallback}"`);
57
+ return fallback;
58
+ }
59
+ console.log(`[XRay LLM] ✅ Final reasoning (${reasoning.length} chars): "${reasoning}"`);
60
+ return reasoning;
61
+ }
62
+ catch (error) {
63
+ console.error(`[XRay LLM] ❌ OpenAI API failed for step ${step.name}:`, error.message);
64
+ const fallback = generateNumericReasoning(step) || `✅ ${step.name} processed`;
65
+ console.log(`[XRay LLM] ✓ Using fallback: "${fallback}"`);
66
+ return fallback;
67
+ }
68
+ };
69
+ }
70
+ /**
71
+ * Simple reasoning generator (no LLM)
72
+ * Returns numeric summaries based on step data
73
+ */
74
+ function createSimpleGenerator() {
75
+ return async (step) => {
76
+ if (step.error) {
77
+ return `❌ ${step.name} failed: ${step.error}`;
78
+ }
79
+ const numericReasoning = generateNumericReasoning(step);
80
+ if (numericReasoning) {
81
+ return numericReasoning;
82
+ }
83
+ return `✅ ${step.name} processed (${step.durationMs ?? 0}ms)`;
84
+ };
85
+ }
86
+ function generateNumericReasoning(step) {
87
+ const input = step.input ?? {};
88
+ const output = step.output ?? {};
89
+ // Ranking/Selection steps
90
+ if (output.ranked_candidates && output.selection) {
91
+ const count = output.ranked_candidates?.length ?? 0;
92
+ const selectionTitle = output.selection?.title ?? output.selection?.asin ?? 'top choice';
93
+ return `Ranked ${count} candidate(s) and selected "${selectionTitle}" as top choice`;
94
+ }
95
+ // Filter pass/fail
96
+ const total = output.total_evaluated ?? output.total_evaluated ?? output.evaluated?.length;
97
+ const passed = output.passed ?? output.accepted ?? output.remaining?.length;
98
+ if (total && passed !== undefined) {
99
+ return `📊 ${passed}/${total} passed`;
100
+ }
101
+ // Search results
102
+ const found = output.total_results ?? output.total_found ?? output.total;
103
+ const returned = output.candidates_fetched ?? output.candidates?.length;
104
+ if (found && returned) {
105
+ return `🔍 ${found}→${returned} results`;
106
+ }
107
+ // Size change (only if different)
108
+ const inputCount = getArrayLength(input);
109
+ const outputCount = getArrayLength(output);
110
+ if (inputCount && outputCount && inputCount !== outputCount) {
111
+ return `🔄 ${inputCount}→${outputCount} items`;
112
+ }
113
+ return null;
114
+ }
115
+ function getArrayLength(obj) {
116
+ if (Array.isArray(obj))
117
+ return obj.length;
118
+ return obj?.candidates?.length ?? obj?.items?.length ?? obj?.remaining?.length ?? obj?.ranked_candidates?.length ?? null;
119
+ }
@@ -0,0 +1,57 @@
1
+ import PQueue from 'p-queue';
2
+ import { ReasoningConfig } from './config';
3
+ import { ReasoningGenerator } from './generator';
4
+ import { ReasoningJob, QueueStats, StorageProvider } from '../types';
5
+ export declare class ReasoningQueue {
6
+ private jobs;
7
+ private queue;
8
+ private config;
9
+ private storage;
10
+ private generator;
11
+ private prisma?;
12
+ constructor(storage: StorageProvider, generator: ReasoningGenerator, config?: Partial<ReasoningConfig>, prismaClient?: any);
13
+ /**
14
+ * Load pending jobs from database (for recovery after restart)
15
+ */
16
+ private loadPendingJobs;
17
+ /**
18
+ * Enqueue a single step for reasoning generation
19
+ */
20
+ enqueue(executionId: string, stepName: string): Promise<string>;
21
+ /**
22
+ * Enqueue all steps from an execution that don't have reasoning
23
+ */
24
+ enqueueExecution(executionId: string): Promise<string[]>;
25
+ /**
26
+ * Process a single job with retry logic
27
+ */
28
+ private processJob;
29
+ /**
30
+ * Handle job error with retry logic
31
+ */
32
+ private handleJobError;
33
+ /**
34
+ * Check if error is retryable
35
+ */
36
+ private isRetryableError;
37
+ /**
38
+ * Process all steps in an execution
39
+ */
40
+ processExecution(executionId: string): Promise<void>;
41
+ /**
42
+ * Get queue statistics
43
+ */
44
+ getStats(): QueueStats;
45
+ /**
46
+ * Get job by ID
47
+ */
48
+ getJob(jobId: string): ReasoningJob | undefined;
49
+ /**
50
+ * Clear all jobs
51
+ */
52
+ clear(): void;
53
+ /**
54
+ * Get underlying p-queue
55
+ */
56
+ get pqueue(): PQueue;
57
+ }
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ReasoningQueue = void 0;
7
+ // Database-backed queue for asynchronous reasoning generation
8
+ const p_queue_1 = __importDefault(require("p-queue"));
9
+ const config_1 = require("./config");
10
+ const crypto_1 = require("crypto");
11
+ class ReasoningQueue {
12
+ constructor(storage, generator, config = {}, prismaClient) {
13
+ this.config = { ...config_1.DEFAULT_REASONING_CONFIG, ...config };
14
+ this.storage = storage;
15
+ this.generator = generator;
16
+ this.prisma = prismaClient;
17
+ this.jobs = new Map();
18
+ this.queue = new p_queue_1.default({ concurrency: this.config.concurrency });
19
+ if (this.config.debug) {
20
+ console.log('[XRay Queue] Reasoning queue initialized', {
21
+ concurrency: this.config.concurrency,
22
+ maxRetries: this.config.maxRetries
23
+ });
24
+ }
25
+ // Load pending jobs from database if available
26
+ if (this.prisma) {
27
+ this.loadPendingJobs().catch(error => {
28
+ console.error('[XRay Queue] Failed to load pending jobs from database:', error);
29
+ });
30
+ }
31
+ }
32
+ /**
33
+ * Load pending jobs from database (for recovery after restart)
34
+ */
35
+ async loadPendingJobs() {
36
+ if (!this.prisma)
37
+ return;
38
+ try {
39
+ const pendingJobs = await this.prisma.reasoningJob.findMany({
40
+ where: {
41
+ status: { in: ['pending', 'processing'] }
42
+ }
43
+ });
44
+ if (pendingJobs.length > 0) {
45
+ console.log(`[XRay Queue] Found ${pendingJobs.length} pending jobs in database, re-enqueuing...`);
46
+ for (const dbJob of pendingJobs) {
47
+ const job = {
48
+ id: dbJob.id,
49
+ executionId: dbJob.executionId,
50
+ stepName: dbJob.stepName,
51
+ attempt: dbJob.attempts,
52
+ status: 'pending',
53
+ createdAt: dbJob.createdAt.toISOString()
54
+ };
55
+ this.jobs.set(job.id, job);
56
+ this.queue.add(() => this.processJob(job.id));
57
+ }
58
+ console.log(`[XRay Queue] Re-enqueued ${pendingJobs.length} jobs from database`);
59
+ }
60
+ }
61
+ catch (error) {
62
+ console.error('[XRay Queue] Failed to load pending jobs from database:', error);
63
+ }
64
+ }
65
+ /**
66
+ * Enqueue a single step for reasoning generation
67
+ */
68
+ async enqueue(executionId, stepName) {
69
+ const jobId = (0, crypto_1.randomUUID)();
70
+ const job = {
71
+ id: jobId,
72
+ executionId,
73
+ stepName,
74
+ attempt: 1,
75
+ status: 'pending',
76
+ createdAt: new Date().toISOString()
77
+ };
78
+ this.jobs.set(jobId, job);
79
+ // Persist job to database if Prisma client is available
80
+ if (this.prisma) {
81
+ try {
82
+ const execution = await this.prisma.execution.findUnique({
83
+ where: { executionId },
84
+ select: { id: true }
85
+ });
86
+ if (execution) {
87
+ await this.prisma.reasoningJob.create({
88
+ data: {
89
+ id: jobId,
90
+ executionId: execution.id,
91
+ stepName,
92
+ status: 'pending',
93
+ attempts: 1
94
+ }
95
+ });
96
+ }
97
+ }
98
+ catch (error) {
99
+ console.error(`[XRay Queue] Failed to persist job to database:`, error);
100
+ }
101
+ }
102
+ // Add to queue
103
+ this.queue.add(() => this.processJob(jobId));
104
+ if (this.config.debug) {
105
+ console.log(`[XRay Queue] Job enqueued: ${executionId}/${stepName}`);
106
+ }
107
+ return jobId;
108
+ }
109
+ /**
110
+ * Enqueue all steps from an execution that don't have reasoning
111
+ */
112
+ async enqueueExecution(executionId) {
113
+ const execution = await this.storage.getExecutionById(executionId);
114
+ if (!execution) {
115
+ throw new Error(`Execution ${executionId} not found`);
116
+ }
117
+ const jobIds = [];
118
+ for (const step of execution.steps) {
119
+ if (!step.reasoning) {
120
+ const jobId = await this.enqueue(executionId, step.name);
121
+ jobIds.push(jobId);
122
+ }
123
+ }
124
+ return jobIds;
125
+ }
126
+ /**
127
+ * Process a single job with retry logic
128
+ */
129
+ async processJob(jobId) {
130
+ const job = this.jobs.get(jobId);
131
+ if (!job) {
132
+ console.error(`[XRay Queue] Job ${jobId} not found`);
133
+ return;
134
+ }
135
+ job.status = 'processing';
136
+ job.startedAt = new Date().toISOString();
137
+ // Update job status in database
138
+ if (this.prisma) {
139
+ try {
140
+ await this.prisma.reasoningJob.update({
141
+ where: { id: jobId },
142
+ data: { status: 'processing' }
143
+ });
144
+ }
145
+ catch (error) {
146
+ console.error(`[XRay Queue] Failed to update job status in database:`, error);
147
+ }
148
+ }
149
+ if (this.config.debug) {
150
+ console.log(`[XRay Queue] Processing job ${jobId} (attempt ${job.attempt}/${this.config.maxRetries})`);
151
+ }
152
+ try {
153
+ // Load execution
154
+ const execution = await this.storage.getExecutionById(job.executionId);
155
+ if (!execution) {
156
+ throw new Error(`Execution ${job.executionId} not found`);
157
+ }
158
+ // Find the step
159
+ const step = execution.steps.find(s => s.name === job.stepName);
160
+ if (!step) {
161
+ throw new Error(`Step ${job.stepName} not found in execution ${job.executionId}`);
162
+ }
163
+ // Skip if reasoning already exists
164
+ if (step.reasoning) {
165
+ console.log(`[XRay Queue] Reasoning already exists for ${job.stepName}, skipping`);
166
+ job.status = 'completed';
167
+ job.completedAt = new Date().toISOString();
168
+ return;
169
+ }
170
+ // Generate reasoning
171
+ const reasoning = await this.generator(step);
172
+ // Update storage
173
+ await this.storage.updateStepReasoning(job.executionId, job.stepName, reasoning);
174
+ // Mark job as completed
175
+ job.status = 'completed';
176
+ job.completedAt = new Date().toISOString();
177
+ // Update job in database
178
+ if (this.prisma) {
179
+ try {
180
+ await this.prisma.reasoningJob.update({
181
+ where: { id: jobId },
182
+ data: {
183
+ status: 'completed',
184
+ reasoning,
185
+ completedAt: new Date()
186
+ }
187
+ });
188
+ }
189
+ catch (error) {
190
+ console.error(`[XRay Queue] Failed to update completed job in database:`, error);
191
+ }
192
+ }
193
+ if (this.config.debug) {
194
+ console.log(`[XRay Queue] ✅ Generated reasoning for ${job.executionId}/${job.stepName}`);
195
+ }
196
+ }
197
+ catch (error) {
198
+ console.error(`[XRay Queue] ❌ Error processing job ${jobId}:`, error.message);
199
+ await this.handleJobError(job, error);
200
+ }
201
+ }
202
+ /**
203
+ * Handle job error with retry logic
204
+ */
205
+ async handleJobError(job, error) {
206
+ const errorMessage = error.message || String(error);
207
+ job.error = errorMessage;
208
+ const isRetryable = this.isRetryableError(error);
209
+ if (isRetryable && job.attempt < this.config.maxRetries) {
210
+ // Schedule retry with exponential backoff
211
+ const delay = this.config.retryDelays[job.attempt - 1] || 8000;
212
+ job.attempt++;
213
+ job.status = 'pending';
214
+ job.nextRetryAt = new Date(Date.now() + delay).toISOString();
215
+ console.warn(`[XRay Queue] Retry ${job.attempt}/${this.config.maxRetries} for ${job.stepName} in ${delay}ms`);
216
+ setTimeout(() => {
217
+ this.queue.add(() => this.processJob(job.id));
218
+ }, delay);
219
+ }
220
+ else {
221
+ // Max retries reached
222
+ job.status = 'failed';
223
+ job.completedAt = new Date().toISOString();
224
+ if (this.prisma) {
225
+ try {
226
+ await this.prisma.reasoningJob.update({
227
+ where: { id: job.id },
228
+ data: {
229
+ status: 'failed',
230
+ error: errorMessage,
231
+ attempts: job.attempt,
232
+ completedAt: new Date()
233
+ }
234
+ });
235
+ }
236
+ catch (dbError) {
237
+ console.error(`[XRay Queue] Failed to update failed job in database:`, dbError);
238
+ }
239
+ }
240
+ console.error(`[XRay Queue] ✗ Failed to generate reasoning for ${job.executionId}/${job.stepName} after ${job.attempt} attempts`);
241
+ }
242
+ }
243
+ /**
244
+ * Check if error is retryable
245
+ */
246
+ isRetryableError(error) {
247
+ const errorMessage = error.message || String(error);
248
+ const errorCode = error.code;
249
+ const retryablePatterns = [
250
+ 'ECONNRESET',
251
+ 'ETIMEDOUT',
252
+ 'ENOTFOUND',
253
+ 'rate_limit_exceeded',
254
+ 'service_unavailable',
255
+ 'timeout',
256
+ '429',
257
+ '503',
258
+ '502',
259
+ ];
260
+ return retryablePatterns.some(pattern => errorMessage.includes(pattern) || errorCode === pattern);
261
+ }
262
+ /**
263
+ * Process all steps in an execution
264
+ */
265
+ async processExecution(executionId) {
266
+ const jobIds = await this.enqueueExecution(executionId);
267
+ if (jobIds.length === 0) {
268
+ if (this.config.debug) {
269
+ console.log(`[XRay Queue] No pending reasoning for execution ${executionId}`);
270
+ }
271
+ return;
272
+ }
273
+ console.log(`[XRay Queue] Processing ${jobIds.length} steps for execution ${executionId}`);
274
+ await this.queue.onIdle();
275
+ console.log(`[XRay Queue] ✓ Completed processing for execution ${executionId}`);
276
+ }
277
+ /**
278
+ * Get queue statistics
279
+ */
280
+ getStats() {
281
+ const jobs = Array.from(this.jobs.values());
282
+ return {
283
+ pending: jobs.filter(j => j.status === 'pending').length,
284
+ processing: jobs.filter(j => j.status === 'processing').length,
285
+ completed: jobs.filter(j => j.status === 'completed').length,
286
+ failed: jobs.filter(j => j.status === 'failed').length,
287
+ totalJobs: jobs.length
288
+ };
289
+ }
290
+ /**
291
+ * Get job by ID
292
+ */
293
+ getJob(jobId) {
294
+ return this.jobs.get(jobId);
295
+ }
296
+ /**
297
+ * Clear all jobs
298
+ */
299
+ clear() {
300
+ this.jobs.clear();
301
+ }
302
+ /**
303
+ * Get underlying p-queue
304
+ */
305
+ get pqueue() {
306
+ return this.queue;
307
+ }
308
+ }
309
+ exports.ReasoningQueue = ReasoningQueue;
@@ -0,0 +1,21 @@
1
+ import { Execution, StorageProvider } from "../types";
2
+ export declare class DatabaseStorage implements StorageProvider {
3
+ private prisma;
4
+ constructor(prismaClient: any);
5
+ /**
6
+ * Save a complete execution to the database
7
+ */
8
+ saveExecution(execution: Execution): Promise<void>;
9
+ /**
10
+ * Get a single execution by ID
11
+ */
12
+ getExecutionById(id: string): Promise<Execution | undefined>;
13
+ /**
14
+ * Get all executions
15
+ */
16
+ getAllExecutions(): Promise<Execution[]>;
17
+ /**
18
+ * Update a single step's reasoning
19
+ */
20
+ updateStepReasoning(executionId: string, stepName: string, reasoning: string): Promise<void>;
21
+ }
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DatabaseStorage = void 0;
4
+ class DatabaseStorage {
5
+ constructor(prismaClient) {
6
+ this.prisma = prismaClient;
7
+ }
8
+ /**
9
+ * Save a complete execution to the database
10
+ */
11
+ async saveExecution(execution) {
12
+ if (!execution || !execution.executionId || execution.steps.length === 0) {
13
+ console.warn("Skipping invalid execution:", execution?.executionId);
14
+ return;
15
+ }
16
+ try {
17
+ const existing = await this.prisma.execution.findUnique({
18
+ where: { executionId: execution.executionId },
19
+ });
20
+ if (existing) {
21
+ await this.prisma.execution.update({
22
+ where: { executionId: execution.executionId },
23
+ data: {
24
+ metadata: execution.metadata || {},
25
+ finalOutcome: execution.finalOutcome || {},
26
+ completedAt: new Date(),
27
+ steps: {
28
+ deleteMany: {},
29
+ create: execution.steps.map(step => ({
30
+ name: step.name,
31
+ input: step.input || {},
32
+ output: step.output || {},
33
+ error: step.error,
34
+ durationMs: step.durationMs,
35
+ reasoning: step.reasoning,
36
+ })),
37
+ },
38
+ },
39
+ });
40
+ console.log(`[XRay Storage] ✅ Updated execution ${execution.executionId}`);
41
+ }
42
+ else {
43
+ await this.prisma.execution.create({
44
+ data: {
45
+ executionId: execution.executionId,
46
+ projectId: execution.metadata?.projectId || "default",
47
+ metadata: execution.metadata || {},
48
+ finalOutcome: execution.finalOutcome || {},
49
+ steps: {
50
+ create: execution.steps.map(step => ({
51
+ name: step.name,
52
+ input: step.input || {},
53
+ output: step.output || {},
54
+ error: step.error,
55
+ durationMs: step.durationMs,
56
+ reasoning: step.reasoning,
57
+ })),
58
+ },
59
+ },
60
+ });
61
+ console.log(`[XRay Storage] ✅ Created execution ${execution.executionId}`);
62
+ }
63
+ }
64
+ catch (error) {
65
+ console.error(`[XRay Storage] ❌ Failed to save execution ${execution.executionId}:`, error);
66
+ throw error;
67
+ }
68
+ }
69
+ /**
70
+ * Get a single execution by ID
71
+ */
72
+ async getExecutionById(id) {
73
+ try {
74
+ const exec = await this.prisma.execution.findUnique({
75
+ where: { executionId: id },
76
+ include: {
77
+ steps: {
78
+ orderBy: {
79
+ createdAt: 'asc',
80
+ },
81
+ },
82
+ },
83
+ });
84
+ if (!exec) {
85
+ return undefined;
86
+ }
87
+ return {
88
+ executionId: exec.executionId,
89
+ startedAt: exec.startedAt.toISOString(),
90
+ endedAt: exec.completedAt?.toISOString(),
91
+ metadata: exec.metadata,
92
+ finalOutcome: exec.finalOutcome,
93
+ steps: exec.steps.map((step) => ({
94
+ name: step.name,
95
+ input: step.input,
96
+ output: step.output,
97
+ error: step.error || undefined,
98
+ durationMs: step.durationMs || undefined,
99
+ reasoning: step.reasoning || undefined,
100
+ timestamp: step.createdAt.toISOString(),
101
+ })),
102
+ };
103
+ }
104
+ catch (error) {
105
+ console.error(`[XRay Storage] ❌ Failed to get execution ${id}:`, error);
106
+ return undefined;
107
+ }
108
+ }
109
+ /**
110
+ * Get all executions
111
+ */
112
+ async getAllExecutions() {
113
+ try {
114
+ const executions = await this.prisma.execution.findMany({
115
+ include: {
116
+ steps: {
117
+ orderBy: {
118
+ createdAt: 'asc',
119
+ },
120
+ },
121
+ },
122
+ orderBy: {
123
+ startedAt: 'desc',
124
+ },
125
+ take: 100,
126
+ });
127
+ return executions.map((exec) => ({
128
+ executionId: exec.executionId,
129
+ startedAt: exec.startedAt.toISOString(),
130
+ endedAt: exec.completedAt?.toISOString(),
131
+ metadata: exec.metadata,
132
+ finalOutcome: exec.finalOutcome,
133
+ steps: exec.steps.map((step) => ({
134
+ name: step.name,
135
+ input: step.input,
136
+ output: step.output,
137
+ error: step.error || undefined,
138
+ durationMs: step.durationMs || undefined,
139
+ reasoning: step.reasoning || undefined,
140
+ timestamp: step.createdAt.toISOString(),
141
+ })),
142
+ }));
143
+ }
144
+ catch (error) {
145
+ console.error("[XRay Storage] ❌ Failed to load executions:", error);
146
+ return [];
147
+ }
148
+ }
149
+ /**
150
+ * Update a single step's reasoning
151
+ */
152
+ async updateStepReasoning(executionId, stepName, reasoning) {
153
+ try {
154
+ const execution = await this.prisma.execution.findUnique({
155
+ where: { executionId },
156
+ include: { steps: true },
157
+ });
158
+ if (!execution) {
159
+ throw new Error(`Execution ${executionId} not found`);
160
+ }
161
+ const step = execution.steps.find((s) => s.name === stepName);
162
+ if (!step) {
163
+ throw new Error(`Step ${stepName} not found in execution ${executionId}`);
164
+ }
165
+ await this.prisma.step.update({
166
+ where: { id: step.id },
167
+ data: { reasoning },
168
+ });
169
+ console.log(`[XRay Storage] ✅ Updated reasoning for ${executionId}/${stepName}`);
170
+ }
171
+ catch (error) {
172
+ console.error(`[XRay Storage] ❌ Failed to update reasoning for ${executionId}/${stepName}:`, error);
173
+ throw error;
174
+ }
175
+ }
176
+ }
177
+ exports.DatabaseStorage = DatabaseStorage;
@@ -0,0 +1,9 @@
1
+ import { Execution, StorageProvider } from "../types";
2
+ export declare class MemoryStorage implements StorageProvider {
3
+ private executions;
4
+ saveExecution(execution: Execution): Promise<void>;
5
+ getExecutionById(executionId: string): Promise<Execution | undefined>;
6
+ getAllExecutions(): Promise<Execution[]>;
7
+ updateStepReasoning(executionId: string, stepName: string, reasoning: string): Promise<void>;
8
+ clear(): void;
9
+ }