You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Upstream half of the post-training data pipeline. Add an ingestion layer that loads raw agent/interaction logs (chat transcripts, tool-call logs, RL rollout dumps) into faithful ContextRecords tagged source="raw", supporting both batch and streaming ingestion. The downstream half — curating those records into trainable datasets and exporting them — is tracked in #96.
lance-context is not where raw training data is generated. But to use it as a curation/staging layer for post-training (#96), raw logs must first land in the store faithfully (lossless) so they can be re-curated into different versioned cuts later — distilling on the way in would throw away the raw and defeat the reproducibility/versioning advantage.
Today the only entry points are add / add_many / upsert of already-shaped records. There is no adapter that maps common raw log formats into ContextRecords, and no documented batch-vs-streaming ingestion path.
Proposed work
Ingestion adapters / normalization — map common raw formats into ContextRecord:
Streaming ingestion — incremental append for live/continuous sources (a queue, a tail, a generator), and for chunked reads of very large dumps without materializing the whole input in memory.
This is a genuinely new execution model, not a wrapper over add_many: add_many eagerly materializes the entire input before writing and embeds the whole batch in one call (see code-reading note below), so it cannot stream an unbounded source or a dump larger than memory.
Real work: bounded-memory chunked reads; configurable commit cadence (records per Lance version / flush interval) — and since each commit is a new version, compaction pressure must be managed; resumability / checkpointing (a cursor/offset so an interrupted ingest resumes without re-appending); partial-failure semantics.
Uniqueness validation must not be O(n) per chunk. The current append path full-scans the dataset on every write to check id/external_id uniqueness (validate_unique_ids), so naive per-chunk add_many is O(n²). Streaming must validate via the id index / a bulk-upsert path instead.
Idempotent ingestion — re-ingesting the same source must not duplicate rows. Note add_manyrejects duplicate external_ids rather than merging them, so this needs chunked upsert/merge semantics or an explicit bulk-upsert path keyed on external_id. (This is identity dedup; semantic dedup/decontamination is curation and lives in Curate lance-context records into trainable datasets (SFT/preference/rollout export) #96.)
Examples + docs
batch: ingest a chat-log JSONL dump into a context
streaming: tail a rollout/event stream into a context with a bounded flush interval and checkpoint/resume
Non-goals
Not a new write path. Reuses add_many / upsert / deferred-embedding as the storage backend; this issue is the adapters + streaming runtime on top, not a replacement for the existing append/upsert APIs.
Bundles no models (no auto-embedding decisions beyond the existing provider registry / deferred-embedding paths).
Acceptance criteria
A user can batch-load chat / tool-call / rollout logs from JSONL/Parquet/Arrow into ContextRecords with preserved provenance and source="raw".
A user can stream-ingest from a continuous source (and from a dump larger than memory) without loading the entire input, with a configurable commit cadence.
Streaming ingest resumes from a checkpoint after interruption without duplicating rows, and does not degrade to a full-store scan per chunk.
Re-ingesting the same source by external_id is idempotent (no duplicate rows).
Adapters preserve the same provenance/state fields across the Python, Rust/core, and REST surfaces (field parity).
Ingested records are deferred-embedding compatible (text first, embeddings later).
Tests cover batch + streaming paths, provenance mapping, field parity, resumability, and idempotency.
Notes
This is the interface contract with #96: ingestion produces faithful ContextRecords; curation/export consumes them. The ContextRecord schema (and the export manifest defined in #96) is the shared boundary — defined once, referenced by both issues. Builds on existing primitives (add_many, batch write #54, deferred embedding #88, embedding provider registry #87) rather than introducing a separate raw-data table.
Implementation audit (2026-06-23)
Validated against the current repo: the issue is legitimate; there are no raw-log ingestion adapters and no documented batch-vs-streaming ingestion layer today.
Clarification for idempotency: idempotent batch/stream re-ingestion remains part of this issue. add_many rejects duplicate external_ids rather than merging them, so ingestion adapters need chunked upsert/merge semantics or an explicit bulk-upsert path.
Clarification for field parity: adapters must preserve provenance and state consistently across Python, Rust/core DTOs, and REST. Some surfaces do not expose exactly the same add/upsert fields today, so field parity should be included in acceptance tests.
Two concrete findings from the current code that shape the streaming design and distinguish this issue from the existing "store records" APIs:
add_many is batch-only, not streaming. The Python wrapper materializes the full input into a list before writing (python/python/lance_context/api.py:657-717) and embeds the whole batch in one provider call (api.py:722-737); the core takes a &[ContextRecord] slice and writes a single version (crates/lance-context-core/src/store.rs:364, :373). There is no streaming path at any layer — bounded-memory streaming is the net-new capability here.
Uniqueness validation is a full table scan per append.validate_unique_ids lists the entire dataset on every add/add_many (crates/lance-context-core/src/store.rs:670 → list_with_options(None, None, ...) at :699) to reject duplicate id/external_id. High-frequency streaming commits via this path would be O(n²); the streaming ingester must validate through the id index or a bulk-upsert path rather than the current full-scan validation.
Summary
Upstream half of the post-training data pipeline. Add an ingestion layer that loads raw agent/interaction logs (chat transcripts, tool-call logs, RL rollout dumps) into faithful
ContextRecords taggedsource="raw", supporting both batch and streaming ingestion. The downstream half — curating those records into trainable datasets and exporting them — is tracked in #96.Motivation
lance-context is not where raw training data is generated. But to use it as a curation/staging layer for post-training (#96), raw logs must first land in the store faithfully (lossless) so they can be re-curated into different versioned cuts later — distilling on the way in would throw away the raw and defeat the reproducibility/versioning advantage.
Today the only entry points are
add/add_many/upsertof already-shaped records. There is no adapter that maps common raw log formats intoContextRecords, and no documented batch-vs-streaming ingestion path.Proposed work
Ingestion adapters / normalization — map common raw formats into
ContextRecord:messages: role/content)role+content_type+state_metadatarelationships/ external-id prefixexternal_id, timestamps →created_at,source="raw", plussession_id/run_id/tenant/bot_idtagsBatch ingestion — bulk-load a file / dataset / directory (JSONL, Parquet, Arrow) into records via chunked atomic appends (build on
add_manyand the batch-write path, Add batch write API for atomic multi-record appends #54). Deferred-embedding compatible (Support deferred embedding workflows for bulk ingestion #88): ingest text now, attach embeddings later.Streaming ingestion — incremental append for live/continuous sources (a queue, a tail, a generator), and for chunked reads of very large dumps without materializing the whole input in memory.
add_many:add_manyeagerly materializes the entire input before writing and embeds the whole batch in one call (see code-reading note below), so it cannot stream an unbounded source or a dump larger than memory.validate_unique_ids), so naive per-chunkadd_manyis O(n²). Streaming must validate via the id index / a bulk-upsert path instead.Idempotent ingestion — re-ingesting the same source must not duplicate rows. Note
add_manyrejects duplicateexternal_ids rather than merging them, so this needs chunked upsert/merge semantics or an explicit bulk-upsert path keyed onexternal_id. (This is identity dedup; semantic dedup/decontamination is curation and lives in Curate lance-context records into trainable datasets (SFT/preference/rollout export) #96.)Examples + docs
Non-goals
add_many/upsert/ deferred-embedding as the storage backend; this issue is the adapters + streaming runtime on top, not a replacement for the existing append/upsert APIs.Acceptance criteria
ContextRecords with preserved provenance andsource="raw".external_idis idempotent (no duplicate rows).Notes
This is the interface contract with #96: ingestion produces faithful
ContextRecords; curation/export consumes them. TheContextRecordschema (and the export manifest defined in #96) is the shared boundary — defined once, referenced by both issues. Builds on existing primitives (add_many, batch write #54, deferred embedding #88, embedding provider registry #87) rather than introducing a separate raw-data table.Implementation audit (2026-06-23)
add_manybatch appends exists (Add batch write API for atomic multi-record appends #54 closed),upsertbyexternal_idexists for single-record idempotent replacement, deferred embeddings exist (Support deferred embedding workflows for bulk ingestion #88 closed), and the embedding provider registry exists (feat: pluggable embedding provider registry #87 merged).add_manyrejects duplicateexternal_ids rather than merging them, so ingestion adapters need chunked upsert/merge semantics or an explicit bulk-upsert path.Streaming design notes (code-reading, 2026-06-23)
Two concrete findings from the current code that shape the streaming design and distinguish this issue from the existing "store records" APIs:
add_manyis batch-only, not streaming. The Python wrapper materializes the full input into a list before writing (python/python/lance_context/api.py:657-717) and embeds the whole batch in one provider call (api.py:722-737); the core takes a&[ContextRecord]slice and writes a single version (crates/lance-context-core/src/store.rs:364,:373). There is no streaming path at any layer — bounded-memory streaming is the net-new capability here.validate_unique_idslists the entire dataset on everyadd/add_many(crates/lance-context-core/src/store.rs:670→list_with_options(None, None, ...)at:699) to reject duplicateid/external_id. High-frequency streaming commits via this path would be O(n²); the streaming ingester must validate through the id index or a bulk-upsert path rather than the current full-scan validation.