Skip to content

TunaRAG Technical Design

Lifecycle and scheduling

Implemented studies move from created to running then completed, with failed and cancelled terminals; a separate stopping state may be added with orchestration. Trials support pending, running, succeeded, failed, abandoned, and cancelled. Terminal states are immutable. Completed studies cannot contain active trials. Retries create attempts instead of moving terminal attempts backward. Trial, attempt, and event sequence numbers are monotonic and never reused.

The scheduler asks the strategy for capacity-sized candidates, validates them through the search space, hashes canonical values, and reserves uniqueness in SQLite. The trial runner prepares the adapter, executes bounded samples, evaluates outputs, aggregates objective and usage, and cleans up under distinct timeout/cancellation boundaries.

The implemented scheduler reserves up to concurrency trials, executes them concurrently, then sorts completed summaries by durable sequence before strategy observation. MaxTrials limits reservations exactly. Cost/time conditions inspect committed snapshots and may overshoot by one in-flight batch. Recovery finishes already reserved trials before new suggestions. Within each trial, the finite evaluation dataset is materialized and executed through a semaphore capped by sample_concurrency; outcomes are folded in dataset order. All observed usage is retained if any sample fails, while partial metrics are discarded. Duplicate metric names within a sample and the reserved tunarag.objective name remain invalid.

Configured callbacks implement async on_event(StudyEvent). Dispatch occurs after commit and follows durable event sequence. Retry-scheduled events are dispatched before sleeping. Callback exceptions are isolated from optimization and recorded as callback.failed with source sequence, callback index/type, and exception type; exception messages are omitted. Failure events are not redispatched, preventing recursive storms. Resume begins callback delivery after the historical event tail.

LLMSearch sends an immutable structured request to an injected provider and validates every response as untrusted input through SearchSpace.validate. Attempts are bounded. Exhaustion uses a stateless deterministic fallback derived from fallback seed and durable suggestion number, which makes resume independent of hidden RNG state. Disabling fallback produces a structured error containing only exception type names. Prior candidate observations are recursively redacted at the provider boundary. Provider semantic configuration participates in resume when it implements fingerprint(). Strategy-provider usage accounting remains an explicit extension because the current strategy contract returns only a candidate.

Retries and resume

Event dispatch acknowledges only the snapshot entries it actually visits. Events committed while a callback awaits remain after the cursor and are delivered by the next dispatch. Advancing directly to the latest durable event would lose concurrent trial completions and their optional tracking projections.

Retry decisions use stage, error code, idempotency, attempt count, and provider hints. Authentication, invalid configuration/output, and missing ground truth are nonretryable; rate limits, transient networks, and provider 5xx responses are generally retryable. Exhausted trial failure does not fail the study by default.

The implemented RetryPolicy retries structured errors explicitly marked retryable plus timeouts and connection failures. Attempt numbers start at zero and every retry starts a new leased attempt. Backoff is deterministic exponential delay capped by policy. A failed retryable attempt commits its observed usage and terminal attempt state in one transaction, while the trial remains running. Sample timeout wraps each adapter and evaluator call; trial timeout wraps complete dataset execution. Cancellation is never converted into a retry.

Resume compares dataset, search-space, objective, adapter, evaluator, strategy, schema, and cache fingerprints; abandons expired attempts; reconstructs budgets; replays terminal results; and continues sequence numbers. Incompatibility requires an explicit fork.

This flow is implemented for created, running, and completed compatible studies. Failed and cancelled studies are terminal. Recovery is scoped to one study, and unexpired attempt leases produce a retryable ResumeError. Terminal summaries are rebuilt from metrics, append-only usage, attempts, trial errors, and cache-hit events. Pending and recoverable trials execute before new suggestions. The durable study.stopping event preserves the result stop reason. Ordinary strategies receive successful observations in sequence order. Replay-aware strategies receive every reserved candidate, including failed and unfinished trials, through the optional replay method. RandomSearch verifies and advances seeded generator state, skipping draws equal to previously replayed candidates because duplicate suggestions never reserve a second trial. An unseen mismatch still fails replay; scanning is bounded to 10,000 draws per reserved candidate to prevent unbounded recovery work.

The implemented store provides attempt leases, persisted abandonment, and a recoverable-trial query. A terminal commit transaction writes metrics, append-only usage, terminal attempt state, terminal trial state, and the corresponding ordered event. Validation failures roll the whole transaction back. Strategy replay consumes ordered committed events only.

Cache, objectives, and stopping

Cache keys use domain-separated SHA-256 over canonical semantic inputs and versions. The implemented foundation stores bytes atomically in an independently versioned SQLite table with namespace, format version, timestamps, privacy class, checksum, state, hit count, and quarantine reason. Reads verify TTL and checksum before exposing bytes. The first corrupt read returns a corrupt outcome and persists quarantine; later reads return quarantined. Expired and quarantined entries remain inspectable until retention pruning. Partial/failed work is not cached without an explicit safe negative-cache policy.

The optimizer cache payload is canonical JSON containing only payload version and aggregated metrics. Objective scores are recomputed so objective weights can change without invalidating evaluation work; cached usage is intentionally empty because result accounting describes the current run. Cache keys include format version, dataset version, candidate, and adapter and evaluator type names. Writes occur only after the durable successful trial commit. Cache read, decode, and write failures append events and do not change trial success. Configurable component fingerprints are included when a component implements fingerprint(); otherwise type identity is used. Mandatory fingerprints for every custom component and TTL selection at the optimizer boundary remain future work.

Evaluator adapters

The core optimizer depends only on the Evaluator protocol. MetricEvaluator normalizes a single sync or async scorer into that protocol and fingerprints callable identity. The optional RagasEvaluator is a boundary adapter, not an engine dependency: it imports RAGAS lazily, creates one SingleTurnSample and one-row EvaluationDataset per engine sample, calls aevaluate, and maps numeric results to namespaced TunaRAG metrics. This preserves engine-owned sample concurrency, timeout, retry, aggregation, and failure-isolation behavior.

An explicit sample mapper is required. TunaRAG never guesses fields from arbitrary adapter outputs or examples, which prevents accidental disclosure and avoids coupling the core domain to RAGAS schemas. Missing/nonfinite RAGAS scores are unavailable rather than zero. Integration errors retain only exception types. RAGAS remains an optional ragas extra pinned to the 0.4 compatibility line; version-specific changes remain localized to evaluators.py.

Dataset ingestion and identity

EvaluationExample is a normalized frozen boundary model. Text normalization is deliberately minimal and meaning-preserving: UTF-8 decode, optional BOM removal, Unicode NFC, CR/CRLF to LF, outer trim, and rejection of controls other than tab/newline. Metadata accepts only finite JSON types and is recursively frozen. File loaders use the standard library and convert only through a declared DatasetFieldMap; they do not infer schemas from content.

Missing sample IDs hash normalized semantic fields in the tunarag:example-id:v1 domain. Dataset versions hash schema version plus ordered semantic examples in the tunarag:dataset:v1 domain. Deterministic split assignment converts the first 64 hash bits of seed plus example/group identity to a fraction and applies configured thresholds. Split selection produces a new dataset version. Source paths and ingestion timestamps never affect identity; malformed record values never enter public error messages.

Synthetic generation

Synthetic generation is a typed provider boundary, not a provider SDK integration. The builder derives a domain-separated seed per document, bounds concurrent calls, gathers in source order, and validates exact response cardinality, span ranges, confidence, and usage. Generated IDs hash document ID, output index, query, answer, and provenance. Every example is labeled synthetic and unreviewed. Source text is transmitted only because the caller explicitly supplies it; exception messages and generated content never enter errors or events.

Objectives preserve direction, normalization, weight, coverage, and confidence. Unknown values do not become zero. Stop rules evaluate a committed snapshot with deterministic precedence and retain the winning reason. Implemented rules cover trial count, score target, cost, wall time, no improvement, search exhaustion, and total/consecutive failure thresholds. Cancellation is an external async control path rather than a polling rule.

StopContext.objective_scores follows durable trial sequence and uses None for failed trials. NoImprovement establishes the first available score as baseline and resets patience only when a later score exceeds best plus min_delta; failures consume patience. Concurrent scheduling checks conditions between committed batches, so every snapshot rule may overshoot by at most the active batch. All built-in rule parameters participate in the study configuration hash.

Open decisions

Python 3.10 structured concurrency, sortable public IDs, sample checkpoint stability, and a future size threshold for external cache payload files remain TBD.

LangChain and LangGraph runnable design

The V0.2 adapters depend structurally on ainvoke and do not import framework classes. A fixed runnable supports cheap runtime configuration; factory(candidate) supports construction-time parameters. Exactly one source is required and every resolved object is validated before use. For canonical examples, the default input is {"question": query} and the default config is {"configurable": candidate.parameters}. Each mapping stage is replaceable independently.

Raw output is preserved unless an output mapper is supplied, ensuring evaluators can inspect answers, contexts, messages, and graph state. Standard message usage is discovered recursively with object-identity deduplication; malformed or negative token counts are ignored rather than invented. An explicit usage mapper replaces discovery. Invocation latency is always appended as a separate framework usage record. Provider exceptions are not wrapped here because the engine already owns retry classification, timeout conversion, redaction, and failure isolation.

Fingerprinting includes callable/type names and explicit caller identity but never serializes framework internals. This avoids credential leakage and unstable hashes. Users must provide identity when two same-type pipelines have different behavior not represented by candidates or mapper identities.

Result reporting and export

OptimizationResult remains an immutable in-memory report and adds derived views rather than persisting duplicate rankings. Objective ranking filters to successful trials with available scores, sorts descending, and uses durable trial sequence for deterministic ties. Metric leaders apply caller-selected direction to available values only. Formal Pareto dominance is deferred because V0.1 has one configured scalar objective and does not define missing-value dominance.

JSON report schema version 1 preserves nested trial metrics and usage. CSV is intentionally lossier and rectangular: one row per trial, JSON candidate cell, flattened usage totals, and lexically ordered dynamic metric columns. Both formats recurse through the central redactor, so Secret values and sensitive keys never leave the result boundary. Exports do not include cache payloads, provider prompts, raw dataset examples, or exception messages beyond already bounded trial error summaries.

MLflow event mirror

MLflow is a projection of committed SQLite state, never an authority. MLflowCallback consumes the same ordered post-commit events as user callbacks. It gathers trial records and metrics on the event-loop thread, then calls the blocking low-level MLflow client through asyncio.to_thread; the SQLite connection never crosses threads. One in-memory run ID is retained per study for the callback lifetime. Terminal study events map to MLflow terminal statuses.

The mirror logs only redacted candidate parameters, numeric metrics, durable sequence, and lifecycle tags. Keys are normalized and hash-suffixed when unsupported characters or length require a change. A missing extra raises MissingOptionalDependencyError, which callback isolation records without failing optimization. Historical events are not replayed on resume, so cross-process continuation intentionally starts a separate run unless a future durable run-ID mapping is introduced.