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 +426 -0
- package/dist/XRay.d.ts +30 -0
- package/dist/XRay.js +95 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +21 -0
- package/dist/reasoning/config.d.ts +8 -0
- package/dist/reasoning/config.js +16 -0
- package/dist/reasoning/generator.d.ts +12 -0
- package/dist/reasoning/generator.js +119 -0
- package/dist/reasoning/queue.d.ts +57 -0
- package/dist/reasoning/queue.js +309 -0
- package/dist/storage/DatabaseStorage.d.ts +21 -0
- package/dist/storage/DatabaseStorage.js +177 -0
- package/dist/storage/MemoryStorage.d.ts +9 -0
- package/dist/storage/MemoryStorage.js +35 -0
- package/dist/types/index.d.ts +59 -0
- package/dist/types/index.js +3 -0
- package/package.json +46 -0
|
@@ -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
|
+
}
|