Skip to content

Allow a dataset to span multiple JSONL files#77

Merged
francescov1 merged 10 commits into
mainfrom
inf-4125-eval-results-dataset
Jul 20, 2026
Merged

Allow a dataset to span multiple JSONL files#77
francescov1 merged 10 commits into
mainfrom
inf-4125-eval-results-dataset

Conversation

@francescov1

@francescov1 francescov1 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Let a single analysis span multiple JSONL dataset files instead of exactly one. TraceStore.load_many(sources) unions several independently-indexed files into one store; every public entrypoint in engine/main.py now accepts trace_path: Path | Sequence[Path]. Single-file TraceStore.load(...) and the single-Path call sites are unchanged.

This keeps the engine generic trace tooling — it has no notion of evals or any other domain. A caller that wants to analyze more than one export (e.g. conversations plus a second file of verdict spans) simply passes both paths; the agent reads them all through the ordinary trace tools and tells them apart by whatever attributes the caller documents in dataset_context.

How

  • engine/traces/trace_store.py: load_many(sources: list[tuple[Path, Path]]) builds a per-file trace-id → file map (_path_by_trace_id, _trace_file_for), and every read (view_trace / view_spans / search_trace / search_span) opens the owning file. _apply_filters groups matching rows by file and opens one handle per file. A trace id that appears in more than one file raises ValueError; an empty source list is rejected.
  • engine/main.py: each entrypoint indexes every path (reusing TraceIndexBuilder) then calls load_many.
  • Tests: tests/unit/traces/test_trace_store_multi_file.py (union counts, per-file reads, cross-file search/overview/filters, duplicate + empty rejection, single-file parity) and a generic second-file fixture tests/fixtures/tiny_traces_second_file.jsonl.

Test plan

  • 464 unit + 64 integration green; ruff + pyright clean.

🤖 Generated with Claude Code


Note

Medium Risk
Breaking public API (trace_pathtrace_paths, index_pathindex_dir) and wide changes to trace I/O and sandbox bootstrap; behavior is guarded by new multi-file and integration tests but callers must update.

Overview
Multi-file datasets are now first-class: all engine/main.py entrypoints take trace_paths: Path | Sequence[Path] (still a single Path for the common case), index each file, and load one logical store with TraceStore.load_many. Trace ids must be unique across files or load fails fast.

TraceDatasetSource is the shared (trace_path, index_path) model for the store, run_python, and sandbox bootstrap JSON-RPC. TraceIndexConfig drops per-run index_path in favor of optional index_dir, with sidecar_index_path placing hashed, collision-safe index files when several traces share a cache directory.

TraceStore routes reads to the owning JSONL file and treats filters/overview/search as one dataset. run_code mounts every file at indexed virtual paths (/input/traces_0.jsonl, …) and bootstraps the same union inside Pyodide as on the host.

Reviewed by Cursor Bugbot for commit 7d93d8a. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread engine/agents/prompt_templates.py Outdated
TraceStore.load_many unions independently-indexed files into one
dataset — every query surface (view/search/filters/overview) treats
the union as one dataset with no file boundaries visible. Trace ids
must be unique across files (load fails fast otherwise). The public
entrypoints accept trace_path as one Path or a sequence.

The engine stays agnostic about what each file encodes — callers
describe their files through dataset_context or their own prompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
@francescov1
francescov1 force-pushed the inf-4125-eval-results-dataset branch from 4ba8b87 to 9a1896a Compare July 18, 2026 13:45
Comment thread engine/main.py
@francescov1 francescov1 changed the title Eval-results dataset support for fixed-pipeline insights runs Allow a dataset to span multiple JSONL files Jul 18, 2026
After a dataset loads via TraceStore.load_many, the run_code sandbox
still bootstrapped a single-file TraceStore from store.trace_path /
store.index_path (the primary file only), so user code saw a trace_store
missing every additional file while the host trace tools saw the union.

Thread all (trace, index) sources through run_python → _run_protocol →
bootstrap → halo_bootstrap: mount each file at an indexed virtual path
and rebuild the store with load_many inside Pyodide. New integration test
mounts two files and asserts the sandbox trace_store sees all 5 traces
and can read one that lives only in the second file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
Comment thread tests/integration/test_sandbox_policy_denials.py Outdated
Comment thread engine/main.py Outdated
francescov1 and others added 4 commits July 18, 2026 18:52
…t path

Two follow-ups to the multi-file sandbox change:

- engine/main.py: a set trace_index.index_path was reused for every file
  in a multi-file dataset, so all files shared one sidecar index and
  load_many paired traces with the wrong byte offsets. Extract
  _resolve_trace_sources: derive a per-file sidecar (the default) and
  fail fast if a single index_path is pinned for a multi-file dataset.
- test_sandbox_policy_denials: after mounts moved to /input/traces_0.jsonl,
  the host-immutability test still wrote to /input/traces.jsonl, so it no
  longer exercised a write against the mounted copy. Point it at the real
  virtual path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
…model

The multi-file bootstrap payload crossed the host↔Pyodide boundary as a
loose JSON array of dicts, indexed by raw key on the Python side. Replace
it with a shared TraceDatasetSource model (living in engine.traces.models
so it's staged into Pyodide alongside the trace store): the host builds
and model_dumps it, and halo_bootstrap validates the JSON back through a
TypeAdapter instead of json.loads + dict access. The wire is still JSON
(it has to cross into WASM as text) but now it's a typed, validated
contract on both ends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
… Path) tuples

Replace the loose (trace_path, index_path) tuple that flowed through the
whole multi-file path with the typed TraceDatasetSource model (now Path
fields, serialized to strings only when crossing the JSON-RPC boundary):

- TraceStore._sources / load_many / sources property
- main._resolve_trace_sources
- Sandbox.run_python / _run_protocol / bootstrap
- halo_bootstrap validates straight into TraceStore.load_many, no tuple rebuild

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
Drop the ``Path | Sequence[Path]`` union (and the isinstance branching it
forced) from every public entrypoint. ``trace_paths: Sequence[Path]`` is
now always a list — one element for the common single-file case, several
for a multi-file dataset — so there's one code path and no single-vs-many
special casing. Callers wrap a lone path as ``[path]``.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
Comment thread engine/main.py
francescov1 and others added 3 commits July 20, 2026 11:35
bf1d1ab made every entrypoint list-only, which TypeErrors when a caller
passes a lone Path (the prior API) — a Path is not a Sequence[Path], so
len()/iteration blow up at startup. Restore the single-path convenience
on the public entrypoints via ``Path | Sequence[Path]`` and normalize once
at the edge (``_as_trace_path_list``); _resolve_trace_sources and every
internal path stay list-only. Callers that pass one file use the bare
Path again; multi-file callers pass a list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
…config

Index location is a per-file property, so move it off the dataset-wide
TraceIndexConfig (which caused the "one index_path can't serve N files"
wart) onto each dataset entry. Entrypoints now take
`Path | Sequence[TraceDataset]` where TraceDataset = {trace_path,
index_path: Path | None}: a lone Path is the single-file edge convenience
(index derived), and a multi-file dataset can pin each file's index
independently. TraceIndexConfig keeps only schema_version;
ensure_index_exists takes the per-file index_path as a param; the
multi-file guard is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
…rride

Revert the per-file TraceDataset index folding in favor of a simpler
single index location that is a directory: TraceIndexConfig.index_dir.
When set, each dataset file's sidecar index is a distinct file named
after the trace inside that one dir, so a single index_dir serves an
entire multi-file dataset (and doubles as a stable cache location, e.g.
for Modal). When unset, each index derives next to its trace as before.

Entrypoints revert to Path | Sequence[Path] (single Path at the edge,
lists internally); TraceDataset is gone; ensure_index_exists derives the
per-file name in index_dir; the multi-file guard is dropped since the
folder handles N files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1f75c96. Configure here.

Comment thread engine/traces/trace_index_builder.py Outdated
Two dataset files sharing a basename in different directories mapped to
the same {index_dir}/{name}.engine-index.jsonl, so one index clobbered
the other and multi-file runs mis-routed byte offsets or hit spurious
duplicate trace_id errors. Derive the in-dir index name from a hash of
the trace's full path (kept in a shared sidecar_index_path helper), so
distinct files get distinct indexes while the name stays deterministic
for stable caching.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwnU9ZBJnrQC89tBumL6PY
@francescov1
francescov1 merged commit da643b9 into main Jul 20, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant