@cephalization/phoenix-insight 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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +620 -0
  3. package/dist/agent/index.js +230 -0
  4. package/dist/cli.js +640 -0
  5. package/dist/commands/index.js +2 -0
  6. package/dist/commands/px-fetch-more-spans.js +98 -0
  7. package/dist/commands/px-fetch-more-trace.js +110 -0
  8. package/dist/config/index.js +165 -0
  9. package/dist/config/loader.js +141 -0
  10. package/dist/config/schema.js +53 -0
  11. package/dist/index.js +1 -0
  12. package/dist/modes/index.js +17 -0
  13. package/dist/modes/local.js +134 -0
  14. package/dist/modes/sandbox.js +121 -0
  15. package/dist/modes/types.js +1 -0
  16. package/dist/observability/index.js +65 -0
  17. package/dist/progress.js +209 -0
  18. package/dist/prompts/index.js +1 -0
  19. package/dist/prompts/system.js +30 -0
  20. package/dist/snapshot/client.js +74 -0
  21. package/dist/snapshot/context.js +332 -0
  22. package/dist/snapshot/datasets.js +68 -0
  23. package/dist/snapshot/experiments.js +135 -0
  24. package/dist/snapshot/index.js +262 -0
  25. package/dist/snapshot/projects.js +44 -0
  26. package/dist/snapshot/prompts.js +199 -0
  27. package/dist/snapshot/spans.js +80 -0
  28. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  29. package/package.json +75 -0
  30. package/src/agent/index.ts +323 -0
  31. package/src/cli.ts +782 -0
  32. package/src/commands/index.ts +8 -0
  33. package/src/commands/px-fetch-more-spans.ts +174 -0
  34. package/src/commands/px-fetch-more-trace.ts +183 -0
  35. package/src/config/index.ts +225 -0
  36. package/src/config/loader.ts +173 -0
  37. package/src/config/schema.ts +66 -0
  38. package/src/index.ts +1 -0
  39. package/src/modes/index.ts +21 -0
  40. package/src/modes/local.ts +163 -0
  41. package/src/modes/sandbox.ts +144 -0
  42. package/src/modes/types.ts +31 -0
  43. package/src/observability/index.ts +90 -0
  44. package/src/progress.ts +239 -0
  45. package/src/prompts/index.ts +1 -0
  46. package/src/prompts/system.ts +31 -0
  47. package/src/snapshot/client.ts +129 -0
  48. package/src/snapshot/context.ts +462 -0
  49. package/src/snapshot/datasets.ts +132 -0
  50. package/src/snapshot/experiments.ts +246 -0
  51. package/src/snapshot/index.ts +403 -0
  52. package/src/snapshot/projects.ts +58 -0
  53. package/src/snapshot/prompts.ts +267 -0
  54. package/src/snapshot/spans.ts +142 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Generates a _context.md summary file for the Phoenix snapshot
3
+ * This provides human and agent-readable context about what data is available
4
+ */
5
+ export async function generateContext(mode, metadata) {
6
+ const lines = [];
7
+ // Header
8
+ lines.push("# Phoenix Snapshot Context");
9
+ lines.push("");
10
+ // Collect stats from the snapshot
11
+ const stats = await collectSnapshotStats(mode);
12
+ // What's Here section
13
+ lines.push("## What's Here");
14
+ // Projects summary
15
+ if (stats.projects.length > 0) {
16
+ const projectSummary = stats.projects
17
+ .map((p) => `${p.name} (${p.spanCount} spans)`)
18
+ .join(", ");
19
+ lines.push(`- **${stats.projects.length} projects**: ${projectSummary}`);
20
+ }
21
+ else {
22
+ lines.push("- **No projects found**");
23
+ }
24
+ // Datasets summary
25
+ if (stats.datasets.length > 0) {
26
+ const datasetNames = stats.datasets.map((d) => d.name).join(", ");
27
+ lines.push(`- **${stats.datasets.length} datasets**: ${datasetNames}`);
28
+ }
29
+ else {
30
+ lines.push("- **No datasets found**");
31
+ }
32
+ // Experiments summary
33
+ if (stats.experiments.length > 0) {
34
+ const completedCount = stats.experiments.filter((e) => e.status === "completed").length;
35
+ const inProgressCount = stats.experiments.filter((e) => e.status === "in_progress").length;
36
+ const failedCount = stats.experiments.filter((e) => e.status === "failed").length;
37
+ const parts = [];
38
+ if (completedCount > 0)
39
+ parts.push(`${completedCount} completed`);
40
+ if (inProgressCount > 0)
41
+ parts.push(`${inProgressCount} in progress`);
42
+ if (failedCount > 0)
43
+ parts.push(`${failedCount} failed`);
44
+ lines.push(`- **${stats.experiments.length} experiments**: ${parts.join(", ")}`);
45
+ }
46
+ else {
47
+ lines.push("- **No experiments found**");
48
+ }
49
+ // Prompts summary
50
+ if (stats.prompts.length > 0) {
51
+ const promptNames = stats.prompts.map((p) => p.name).join(", ");
52
+ lines.push(`- **${stats.prompts.length} prompts**: ${promptNames}`);
53
+ }
54
+ else {
55
+ lines.push("- **No prompts found**");
56
+ }
57
+ // Snapshot metadata
58
+ lines.push(`- **Snapshot**: Created ${formatRelativeTime(metadata.snapshotTime)} from ${metadata.phoenixUrl}`);
59
+ lines.push("");
60
+ // Recent Activity section (if we have recent data)
61
+ const recentActivity = getRecentActivity(stats);
62
+ if (recentActivity.length > 0) {
63
+ lines.push("## Recent Activity");
64
+ for (const activity of recentActivity) {
65
+ lines.push(`- ${activity}`);
66
+ }
67
+ lines.push("");
68
+ }
69
+ // What You Can Do section
70
+ lines.push("## What You Can Do");
71
+ lines.push("- **Explore**: ls, cat, grep, find, jq, awk, sed");
72
+ lines.push("- **Fetch more data**: `px-fetch-more spans --project <name> --limit 500`");
73
+ lines.push("- **Fetch specific trace**: `px-fetch-more trace --trace-id <id>`");
74
+ lines.push("");
75
+ // Data Freshness section
76
+ lines.push("## Data Freshness");
77
+ lines.push("This is a **read-only snapshot**. Data may have changed since capture.");
78
+ lines.push("Run with `--refresh` to get latest data.");
79
+ lines.push("");
80
+ // File Formats section
81
+ lines.push("## File Formats");
82
+ lines.push("- `.jsonl` files: One JSON object per line, use `jq -s` to parse as array");
83
+ lines.push("- `.json` files: Standard JSON");
84
+ lines.push("- `.md` files: Markdown (prompt templates)");
85
+ lines.push("");
86
+ // Directory Structure section
87
+ lines.push("## Directory Structure");
88
+ lines.push("```");
89
+ lines.push("/phoenix/");
90
+ lines.push(" _context.md # This file");
91
+ lines.push(" /projects/");
92
+ lines.push(" index.jsonl # List of all projects");
93
+ lines.push(" /{project_name}/");
94
+ lines.push(" metadata.json # Project details");
95
+ lines.push(" /spans/");
96
+ lines.push(" index.jsonl # Span data (may be sampled)");
97
+ lines.push(" metadata.json # Span snapshot metadata");
98
+ lines.push(" /datasets/");
99
+ lines.push(" index.jsonl # List of all datasets");
100
+ lines.push(" /{dataset_name}/");
101
+ lines.push(" metadata.json # Dataset details");
102
+ lines.push(" examples.jsonl # Dataset examples");
103
+ lines.push(" /experiments/");
104
+ lines.push(" index.jsonl # List of all experiments");
105
+ lines.push(" /{experiment_id}/");
106
+ lines.push(" metadata.json # Experiment details");
107
+ lines.push(" runs.jsonl # Experiment runs");
108
+ lines.push(" /prompts/");
109
+ lines.push(" index.jsonl # List of all prompts");
110
+ lines.push(" /{prompt_name}/");
111
+ lines.push(" metadata.json # Prompt details");
112
+ lines.push(" /versions/");
113
+ lines.push(" index.jsonl # Version list");
114
+ lines.push(" /{version_id}.md # Version template");
115
+ lines.push(" /_meta/");
116
+ lines.push(" snapshot.json # Snapshot metadata");
117
+ lines.push("```");
118
+ // Write the context file
119
+ await mode.writeFile("/phoenix/_context.md", lines.join("\n"));
120
+ }
121
+ /**
122
+ * Collects statistics from the snapshot filesystem
123
+ */
124
+ async function collectSnapshotStats(mode) {
125
+ const result = {
126
+ projects: [],
127
+ datasets: [],
128
+ experiments: [],
129
+ prompts: [],
130
+ };
131
+ // Collect project stats
132
+ try {
133
+ const projectsExec = await mode.exec("cat /phoenix/projects/index.jsonl 2>/dev/null || true");
134
+ if (projectsExec.stdout) {
135
+ const projectLines = projectsExec.stdout
136
+ .trim()
137
+ .split("\n")
138
+ .filter((line) => line.length > 0);
139
+ for (const line of projectLines) {
140
+ try {
141
+ const project = JSON.parse(line);
142
+ const stats = {
143
+ name: project.name,
144
+ spanCount: 0,
145
+ };
146
+ // Get span count for this project
147
+ const spansMetaExec = await mode.exec(`cat /phoenix/projects/${project.name}/spans/metadata.json 2>/dev/null || echo "{}"`);
148
+ if (spansMetaExec.stdout) {
149
+ try {
150
+ const spansMeta = JSON.parse(spansMetaExec.stdout);
151
+ stats.spanCount = spansMeta.spanCount || 0;
152
+ }
153
+ catch (e) {
154
+ // Ignore parse errors
155
+ }
156
+ }
157
+ result.projects.push(stats);
158
+ }
159
+ catch (e) {
160
+ // Skip invalid project lines
161
+ }
162
+ }
163
+ }
164
+ }
165
+ catch (e) {
166
+ // No projects file
167
+ }
168
+ // Collect dataset stats
169
+ try {
170
+ const datasetsExec = await mode.exec("cat /phoenix/datasets/index.jsonl 2>/dev/null || true");
171
+ if (datasetsExec.stdout) {
172
+ const datasetLines = datasetsExec.stdout
173
+ .trim()
174
+ .split("\n")
175
+ .filter((line) => line.length > 0);
176
+ for (const line of datasetLines) {
177
+ try {
178
+ const dataset = JSON.parse(line);
179
+ // Get example count
180
+ const examplesExec = await mode.exec(`wc -l < /phoenix/datasets/${dataset.name}/examples.jsonl 2>/dev/null || echo "0"`);
181
+ const exampleCount = parseInt(examplesExec.stdout.trim()) || 0;
182
+ result.datasets.push({
183
+ name: dataset.name,
184
+ exampleCount,
185
+ updatedAt: dataset.updated_at,
186
+ });
187
+ }
188
+ catch (e) {
189
+ // Skip invalid dataset lines
190
+ }
191
+ }
192
+ }
193
+ }
194
+ catch (e) {
195
+ // No datasets file
196
+ }
197
+ // Collect experiment stats
198
+ try {
199
+ const experimentsExec = await mode.exec("cat /phoenix/experiments/index.jsonl 2>/dev/null || true");
200
+ if (experimentsExec.stdout) {
201
+ const experimentLines = experimentsExec.stdout
202
+ .trim()
203
+ .split("\n")
204
+ .filter((line) => line.length > 0);
205
+ for (const line of experimentLines) {
206
+ try {
207
+ const experiment = JSON.parse(line);
208
+ const status = determineExperimentStatus(experiment);
209
+ result.experiments.push({
210
+ id: experiment.id,
211
+ datasetName: experiment.datasetName || "unknown",
212
+ projectName: experiment.project_name,
213
+ status,
214
+ runCounts: {
215
+ successful: experiment.successful_run_count || 0,
216
+ failed: experiment.failed_run_count || 0,
217
+ missing: experiment.missing_run_count || 0,
218
+ },
219
+ updatedAt: experiment.updated_at,
220
+ });
221
+ }
222
+ catch (e) {
223
+ // Skip invalid experiment lines
224
+ }
225
+ }
226
+ }
227
+ }
228
+ catch (e) {
229
+ // No experiments file
230
+ }
231
+ // Collect prompt stats
232
+ try {
233
+ const promptsExec = await mode.exec("cat /phoenix/prompts/index.jsonl 2>/dev/null || true");
234
+ if (promptsExec.stdout) {
235
+ const promptLines = promptsExec.stdout
236
+ .trim()
237
+ .split("\n")
238
+ .filter((line) => line.length > 0);
239
+ for (const line of promptLines) {
240
+ try {
241
+ const prompt = JSON.parse(line);
242
+ // Count versions
243
+ const versionsExec = await mode.exec(`wc -l < /phoenix/prompts/${prompt.name}/versions/index.jsonl 2>/dev/null || echo "0"`);
244
+ const versionCount = parseInt(versionsExec.stdout.trim()) || 0;
245
+ result.prompts.push({
246
+ name: prompt.name,
247
+ versionCount,
248
+ updatedAt: prompt.updated_at,
249
+ });
250
+ }
251
+ catch (e) {
252
+ // Skip invalid prompt lines
253
+ }
254
+ }
255
+ }
256
+ }
257
+ catch (e) {
258
+ // No prompts file
259
+ }
260
+ return result;
261
+ }
262
+ /**
263
+ * Determines the status of an experiment based on its run counts
264
+ */
265
+ function determineExperimentStatus(experiment) {
266
+ const totalExpected = experiment.example_count * experiment.repetitions;
267
+ const totalRuns = (experiment.successful_run_count || 0) + (experiment.failed_run_count || 0);
268
+ if (totalRuns === 0) {
269
+ return "in_progress";
270
+ }
271
+ // If most runs are failed, consider it failed
272
+ if ((experiment.failed_run_count || 0) > (experiment.successful_run_count || 0)) {
273
+ return "failed";
274
+ }
275
+ if (totalRuns >= totalExpected) {
276
+ return "completed";
277
+ }
278
+ return "in_progress";
279
+ }
280
+ /**
281
+ * Gets recent activity highlights
282
+ */
283
+ function getRecentActivity(stats) {
284
+ const activities = [];
285
+ // Find recently updated experiments
286
+ const recentExperiments = stats.experiments
287
+ .filter((e) => e.updatedAt && isRecent(new Date(e.updatedAt), 24))
288
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
289
+ for (const exp of recentExperiments.slice(0, 2)) {
290
+ const timeAgo = formatRelativeTime(new Date(exp.updatedAt));
291
+ activities.push(`${exp.projectName || exp.datasetName}: experiment "${exp.id.slice(0, 8)}..." ${exp.status} ${timeAgo}`);
292
+ }
293
+ // Find recently updated datasets
294
+ const recentDatasets = stats.datasets
295
+ .filter((d) => d.updatedAt && isRecent(new Date(d.updatedAt), 24))
296
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
297
+ for (const dataset of recentDatasets.slice(0, 2)) {
298
+ const timeAgo = formatRelativeTime(new Date(dataset.updatedAt));
299
+ activities.push(`${dataset.name}: dataset updated ${timeAgo} (${dataset.exampleCount} examples)`);
300
+ }
301
+ return activities.slice(0, 3); // Limit to 3 activities
302
+ }
303
+ /**
304
+ * Checks if a date is within the specified hours from now
305
+ */
306
+ function isRecent(date, hoursAgo) {
307
+ const now = new Date();
308
+ const diff = now.getTime() - date.getTime();
309
+ return diff < hoursAgo * 60 * 60 * 1000;
310
+ }
311
+ /**
312
+ * Formats a date as relative time (e.g., "2 hours ago")
313
+ */
314
+ function formatRelativeTime(date) {
315
+ const now = new Date();
316
+ const diff = now.getTime() - date.getTime();
317
+ const minutes = Math.floor(diff / (1000 * 60));
318
+ const hours = Math.floor(diff / (1000 * 60 * 60));
319
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
320
+ if (minutes < 1) {
321
+ return "just now";
322
+ }
323
+ else if (minutes < 60) {
324
+ return `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
325
+ }
326
+ else if (hours < 24) {
327
+ return `${hours} hour${hours !== 1 ? "s" : ""} ago`;
328
+ }
329
+ else {
330
+ return `${days} day${days !== 1 ? "s" : ""} ago`;
331
+ }
332
+ }
@@ -0,0 +1,68 @@
1
+ import { withErrorHandling, extractData } from "./client.js";
2
+ /**
3
+ * Converts an array to JSONL format
4
+ */
5
+ function toJSONL(items) {
6
+ if (items.length === 0) {
7
+ return "";
8
+ }
9
+ return items.map((item) => JSON.stringify(item)).join("\n");
10
+ }
11
+ /**
12
+ * Fetches all datasets and their examples from Phoenix
13
+ */
14
+ export async function fetchDatasets(client, mode, options = {}) {
15
+ const { limit = 100 } = options;
16
+ // Fetch all datasets with pagination
17
+ const datasets = [];
18
+ let cursor = null;
19
+ while (datasets.length < limit) {
20
+ const query = {
21
+ limit: Math.min(limit - datasets.length, 100),
22
+ };
23
+ if (cursor) {
24
+ query.cursor = cursor;
25
+ }
26
+ const response = await withErrorHandling(() => client.GET("/v1/datasets", { params: { query } }), "fetching datasets");
27
+ const data = extractData(response);
28
+ datasets.push(...data.data);
29
+ cursor = data.next_cursor;
30
+ // Stop if no more data
31
+ if (!cursor || data.data.length === 0) {
32
+ break;
33
+ }
34
+ }
35
+ // Write datasets index
36
+ await mode.writeFile("/phoenix/datasets/index.jsonl", toJSONL(datasets));
37
+ // Fetch examples for each dataset
38
+ for (const dataset of datasets) {
39
+ // Write dataset metadata
40
+ await mode.writeFile(`/phoenix/datasets/${dataset.name}/metadata.json`, JSON.stringify({
41
+ id: dataset.id,
42
+ name: dataset.name,
43
+ description: dataset.description,
44
+ metadata: dataset.metadata,
45
+ created_at: dataset.created_at,
46
+ updated_at: dataset.updated_at,
47
+ snapshot_timestamp: new Date().toISOString(),
48
+ }, null, 2));
49
+ // Fetch examples for this dataset
50
+ const examplesResponse = await withErrorHandling(() => client.GET("/v1/datasets/{id}/examples", {
51
+ params: {
52
+ path: { id: dataset.id },
53
+ },
54
+ }), `fetching examples for dataset ${dataset.name}`);
55
+ const examplesData = extractData(examplesResponse);
56
+ const examples = examplesData.data.examples;
57
+ // Write examples as JSONL
58
+ await mode.writeFile(`/phoenix/datasets/${dataset.name}/examples.jsonl`, toJSONL(examples));
59
+ // Write dataset info with example count
60
+ await mode.writeFile(`/phoenix/datasets/${dataset.name}/info.json`, JSON.stringify({
61
+ dataset_id: dataset.id,
62
+ dataset_name: dataset.name,
63
+ example_count: examples.length,
64
+ version_id: examplesData.data.version_id,
65
+ filtered_splits: examplesData.data.filtered_splits,
66
+ }, null, 2));
67
+ }
68
+ }
@@ -0,0 +1,135 @@
1
+ import { withErrorHandling, extractData } from "./client.js";
2
+ /**
3
+ * Converts an array to JSONL format
4
+ */
5
+ function toJSONL(items) {
6
+ if (items.length === 0) {
7
+ return "";
8
+ }
9
+ return items.map((item) => JSON.stringify(item)).join("\n");
10
+ }
11
+ /**
12
+ * Fetches all experiments and their runs from Phoenix
13
+ * Note: Experiments are fetched per dataset since there's no direct "all experiments" endpoint
14
+ */
15
+ export async function fetchExperiments(client, mode, options = {}) {
16
+ const { limit = 100, includeRuns = true } = options;
17
+ // First, we need to get all datasets to fetch their experiments
18
+ const datasetsResponse = await withErrorHandling(() => client.GET("/v1/datasets", { params: { query: { limit: 1000 } } }), "fetching datasets for experiments");
19
+ const datasetsData = extractData(datasetsResponse);
20
+ const datasets = datasetsData.data;
21
+ // Collect all experiments from all datasets
22
+ const allExperiments = [];
23
+ for (const dataset of datasets) {
24
+ try {
25
+ // Fetch experiments for this dataset with pagination
26
+ const experiments = [];
27
+ let cursor = null;
28
+ do {
29
+ const response = await withErrorHandling(() => client.GET("/v1/datasets/{dataset_id}/experiments", {
30
+ params: {
31
+ path: {
32
+ dataset_id: dataset.id,
33
+ },
34
+ query: {
35
+ cursor,
36
+ limit: 50,
37
+ },
38
+ },
39
+ }), `fetching experiments for dataset ${dataset.name}`);
40
+ const data = extractData(response);
41
+ experiments.push(...(data.data || []));
42
+ cursor = data.next_cursor || null;
43
+ // Stop if we've reached the overall limit
44
+ if (allExperiments.length + experiments.length >= limit) {
45
+ const remaining = limit - allExperiments.length;
46
+ experiments.splice(remaining);
47
+ cursor = null;
48
+ }
49
+ } while (cursor != null);
50
+ // Add dataset name to each experiment for context
51
+ const experimentsWithDatasetName = experiments.map((exp) => ({
52
+ ...exp,
53
+ datasetName: dataset.name,
54
+ }));
55
+ allExperiments.push(...experimentsWithDatasetName);
56
+ // Apply limit if specified
57
+ if (allExperiments.length >= limit) {
58
+ break;
59
+ }
60
+ }
61
+ catch (error) {
62
+ // If fetching experiments for a dataset fails, log and continue
63
+ console.warn(`Failed to fetch experiments for dataset ${dataset.name}:`, error);
64
+ }
65
+ }
66
+ // Write experiments index
67
+ await mode.writeFile("/phoenix/experiments/index.jsonl", toJSONL(allExperiments));
68
+ // Fetch runs for each experiment if requested
69
+ if (includeRuns) {
70
+ for (const experiment of allExperiments) {
71
+ try {
72
+ // Write experiment metadata
73
+ await mode.writeFile(`/phoenix/experiments/${experiment.id}/metadata.json`, JSON.stringify({
74
+ id: experiment.id,
75
+ dataset_id: experiment.dataset_id,
76
+ dataset_name: experiment.datasetName,
77
+ dataset_version_id: experiment.dataset_version_id,
78
+ repetitions: experiment.repetitions,
79
+ metadata: experiment.metadata,
80
+ project_name: experiment.project_name,
81
+ created_at: experiment.created_at,
82
+ updated_at: experiment.updated_at,
83
+ example_count: experiment.example_count,
84
+ successful_run_count: experiment.successful_run_count,
85
+ failed_run_count: experiment.failed_run_count,
86
+ missing_run_count: experiment.missing_run_count,
87
+ snapshot_timestamp: new Date().toISOString(),
88
+ }, null, 2));
89
+ // Fetch runs for this experiment with pagination
90
+ const runs = [];
91
+ let cursor = null;
92
+ do {
93
+ const runsResponse = await withErrorHandling(() => client.GET("/v1/experiments/{experiment_id}/runs", {
94
+ params: {
95
+ path: {
96
+ experiment_id: experiment.id,
97
+ },
98
+ query: {
99
+ cursor,
100
+ limit: 100,
101
+ },
102
+ },
103
+ }), `fetching runs for experiment ${experiment.id}`);
104
+ const runsData = extractData(runsResponse);
105
+ runs.push(...(runsData.data || []));
106
+ cursor = runsData.next_cursor || null;
107
+ } while (cursor != null);
108
+ // Write runs as JSONL
109
+ await mode.writeFile(`/phoenix/experiments/${experiment.id}/runs.jsonl`, toJSONL(runs));
110
+ // Write experiment summary with run stats
111
+ await mode.writeFile(`/phoenix/experiments/${experiment.id}/summary.json`, JSON.stringify({
112
+ experiment_id: experiment.id,
113
+ dataset_name: experiment.datasetName,
114
+ project_name: experiment.project_name,
115
+ total_runs: runs.length,
116
+ successful_runs: experiment.successful_run_count,
117
+ failed_runs: experiment.failed_run_count,
118
+ missing_runs: experiment.missing_run_count,
119
+ created_at: experiment.created_at,
120
+ updated_at: experiment.updated_at,
121
+ }, null, 2));
122
+ }
123
+ catch (error) {
124
+ // If fetching runs for an experiment fails, log and continue
125
+ console.warn(`Failed to fetch runs for experiment ${experiment.id}:`, error);
126
+ // Still create the experiment metadata without runs
127
+ await mode.writeFile(`/phoenix/experiments/${experiment.id}/metadata.json`, JSON.stringify({
128
+ ...experiment,
129
+ error: "Failed to fetch runs",
130
+ snapshot_timestamp: new Date().toISOString(),
131
+ }, null, 2));
132
+ }
133
+ }
134
+ }
135
+ }