Skip to content

Releases

v0.3.0

Choose a tag to compare

@milliondreams milliondreams released this 26 Sep 18:22
6252473

[0.3.0] - 2026-09-26

Breaking

  • ModalityExtractor::extract is replaced by prepare + apply_in_tx,
    with a new defaulted finish_post_commit. Attachments now commit inside the
    same transaction as the turns carrying them, which requires uniko to own that
    transaction. A tx parameter on extract would not be enough: a unit must
    know every content_id it will merge before opening the transaction, so it
    can take the striped locks first, and a single call gives it no point at
    which to ask. The split also keeps decoding and model inference outside the
    transaction, where a retry does not re-pay them.

    No default body can soften this. One forwarding to the old extract would
    have to open its own transaction — exactly the non-atomicity this removes —
    and would silently degrade a host that believed it had migrated. Implementors
    get a hard error at the impl block instead.

    // before
    async fn extract(&self, kb: &KnowledgeBase, src: &IngestSource)
        -> Result<ArtifactIngestResult, UnikoError>
    {
        let caption = self.vlm.caption(src).await?;          // model
        let nid = kb.create_node("Artifact", &props).await?;  // commits
        Ok(ArtifactIngestResult { artifact_node_id: nid, .. })
    }
    
    // after
    async fn prepare(&self, kb: &KnowledgeBase, src: &IngestSource)
        -> Result<Box<dyn ModalityPrepared>, UnikoError>
    {
        let caption = self.vlm.caption(src).await?;   // model, no tx open
        let put = kb.put_blob(&hash, &bytes).await?;  // blob I/O
        Ok(Box::new(MyPrep { hash, size, caption, put }))
    }
    
    async fn apply_in_tx(
        &self, kb: &KnowledgeBase, tx: &Transaction,
        prep: &dyn ModalityPrepared,
        ctx: ArtifactContextNids, seen: &mut UnitArtifactSeen,
    ) -> Result<ArtifactIngestResult, UnikoError> {
        let prep = prep.as_any().downcast_ref::<MyPrep>().expect("own prep");
        let nid = kb.create_node_in_tx(tx, "Artifact", &props).await?;
        // ctx.message_nid is already resolved — do NOT look it up by id.
        Ok(ArtifactIngestResult { artifact_node_id: nid, .. })
    }

    Post-commit work — such as Artifact.image_embedding, which has no embedding
    config and is written through a self-committing update — moves to
    finish_post_commit. Python bindings are unaffected: Python-authored
    extractors were never exposed.

  • Session::summarize returns SummarizeReport rather than
    Option<NodeId>; the Python binding returns (summary_id, finalize_error).
    The chunk refresh it performs is still best-effort — failing summary
    generation over a transient conflict would regress existing callers — but its
    outcome is now reported instead of only logged, so a caller can tell that a
    summary was built from stale chunks.

Added

  • Source revisions and retirement (rustic-ai/uniko#41):

    // A newer revision of the same source replaces the previous one.
    session.ingest(
        IngestSource::text(page_v2).with_source("wiki-summit").with_revision("rev-b"),
    ).await?;
    
    agent.recall("summit elevation").await?;                          // rev-b only
    agent.recall_in("summit elevation",
        Scope::default().include_superseded()).await?;                // history too
    agent.retire_source("wiki-summit").await?;                        // neither

    IngestSource::with_revision / Turn::revision declare an immutable
    revision identity. Together with the existing content fingerprint it
    decides idempotency: the same revision with identical bytes is a no-op, and
    the same revision with changed bytes is rejected with IdConflict, because
    a revision id is a promise about the content.

    A new revision of a source stamps superseded_at on the previous one and
    records SUPERSEDES from new to old. Agent::retire_source stamps
    Source.retired_at. Ordinary recall excludes both, so replaced or
    retired evidence cannot ground a current answer, while
    Scope::include_superseded reaches it so an older result stays
    attributable to the revision that grounded it. Nothing is deleted.

    Landing this does not change anyone's results on its own: the exclusion set
    is resolved first, and when nothing has been retired or superseded it comes
    back empty and no predicate is emitted at all. Results shrink only once
    something has actually been retired or replaced.

    Retirement is recorded on the :Source, never on the content, so retiring
    one source cannot affect another that merely shares identical bytes — those
    share an :ArtifactContent row but not a :Source.

    SUPERSEDES was already declared Rule → Rule; uni-db permits one
    declaration per edge type, so both endpoint pairs are now declared
    together. Schema node and edge counts are unchanged.

  • Typed record provenance and pre-ranking recall filters
    (rustic-ai/uniko#39):

    session.observe(
        Turn::new("agent", "the query returned 42 rows")
            .category("executed_result")
            .source("warehouse-prod"),
    ).await?;
    
    let scope = Scope::default().categories(["executed_result"]);
    let bundle = agent.recall_in("row counts", scope).await?;

    A caller can record a record class and a stable logical source without
    placing either in searchable prose. A new :Source node holds the logical
    identity, with FROM_SOURCE from the Message or Artifact; category and
    source_id are denormalised onto Message, Artifact, Chunk, Observation and
    Fact so the filter is a property predicate rather than a traversal.

    The filters resolve into the allow-set that candidate generation itself
    uses, so ranking, the result limit and the coverage score describe the
    permitted evidence — not items that ranked well and were discarded after
    Uniko had already scored them. A filter with no eligible matches returns
    empty rather than being padded with other categories. Every RecallItem
    reports its own category and source_id; None means "none recorded" or
    "no single traceable source", not "filtered out".

    Episode carries no provenance, so a provenance filter excludes it,
    matching the documented rule that a node which cannot anchor a dimension
    goes dark under a filter on it. Artifact and Fact become reachable by a
    filter for the first time: neither anchors session, participant or time, so
    the older dimensions had no arm for them.

    Schema is now 26 node types, 55 edge types.

  • Atomic, idempotent multi-turn units (rustic-ai/uniko#40):

    session.unit()
        .turn(Turn::new("alice", "what's the plan?").id("m-1"))
        .turn(Turn::new("bob", "ship it friday").id("m-2"))
        .commit().await?;

    Every message, its edges and chunks, the unit's merged entity upsert, each
    turn's mentions and observations, and every attachment are written in ONE
    transaction that commits once. A related pair is either wholly visible to
    later recall or wholly absent — the guarantee two separate observe calls
    cannot give, where an interruption leaves a question with no answer.

    Re-committing a unit whose ids are all present with identical content is a
    no-op. Reusing an id with different content, repeating an id within one unit,
    or submitting a unit that is only partly recorded is rejected: a correct
    caller cannot produce a partial unit, and both alternatives — ingesting only
    the absent turns, or treating the whole unit as a no-op — silently corrupt.

    Session::observe is now a one-turn unit, so the two entry points cannot
    drift. Two behaviour changes follow: attachments commit with their
    message rather than in separate transactions afterwards, and an idempotent
    replay advances the chain head so the following turn still gets its NEXT
    edge.

  • Agent::finalize_session quiesces the streaming pipeline before reporting,
    so it cannot claim completion while ingest for that session is still in
    flight. The barrier is instance-wide rather than per-session: broader than
    needed, but it never waits too little.

Fixed

  • Observation ordering was not a total order. session_observation_rows
    ordered by the message's timestamp, which every observation from that
    message shares, so their relative order was arbitrary. The observation chunk
    surface concatenates those rows, so a reordering changed the chunk text and
    finalize() rebuilt an unchanged session — re-embedding every observation
    chunk, with nothing to explain why. Since FinalizeReport::rebuilt ORs the
    transcript and observation surfaces, this surfaced as a finalize claiming to
    have rewritten a session nobody had touched.

  • Session chunking no longer degrades failed reads into wrong data.
    session_chunk_rows dropped a row whose id failed to decode, making an
    unreadable chunk surface look absent, and defaulted unreadable text to
    "", making a readable chunk look changed; session_transcript_rows
    substituted "unknown" for an unreadable speaker name, rewriting the
    transcript that the idempotency check compares. All three now propagate. A
    genuinely NULL speaker still reads as "unknown" — that case is legitimate.

  • Artifact ingest is atomic. The node, its HAS_CONTENT edge, the
    provenance edges and the chunks were written in four-plus separate commits,
    so a failure between them left a committed Artifact with no chunks. Both the
    text and PDF paths now write in one transaction; for the tiered PDF path this
    also collapses what was on the order of 2*pages + 4*blocks + chunks
    separate commits into a single one.

  • PDF provenance edges could vanish silently. link_pdf_context skipped an
    unresolvable reference with no output at all — not even the debug line the
    artifact path emits — wrote ATTACHED_TO ...

Read more

v0.2.1

Choose a tag to compare

@milliondreams milliondreams released this 02 Sep 19:39
98999a2

What's Changed

Full Changelog: v0.2.0...v0.2.1

What's Changed

Full Changelog: v0.2.0...v0.2.1

v0.2.0

Choose a tag to compare

@milliondreams milliondreams released this 04 Aug 07:23
f399004

What's Changed

  • ci: publish PEP 503 package index for release wheels by @Shiti in #24
  • Bump uni-db to 3.2.0, enhance CI, and update documentation by @milliondreams in #27
  • Regenerate schema snapshot and fix configuration issues by @milliondreams in #28

New Contributors

  • @Shiti made their first contribution in #24

Full Changelog: v0.1.1...v0.2.0

What's Changed

  • ci: publish PEP 503 package index for release wheels by @Shiti in #24
  • Bump uni-db to 3.2.0, enhance CI, and update documentation by @milliondreams in #27
  • Regenerate schema snapshot and fix configuration issues by @milliondreams in #28

New Contributors

  • @Shiti made their first contribution in #24

Full Changelog: v0.1.1...v0.2.0

v0.1.1

Choose a tag to compare

@milliondreams milliondreams released this 08 Jul 08:07
v0.1.1
269e8fc

First public release.

uniko is an embedded, Rust-native cognitive memory system for AI agents. It turns
conversations into structured, searchable, reasoned-over knowledge — with no LLM in
the recall hot path
. It links into the host process like SQLite and requires no
external infrastructure (no Neo4j, no Qdrant, no PostgreSQL): a single in-process
graph + vector + full-text store with a Locy logic engine. Compile knowledge in once
at write time; query it forever.

Built on uni-db (embedded multi-model graph
database) and uni-xervo (model runtime for embeddings, NLP, and reranking).

Distribution & packaging

  • Rust crates on crates.io. Six layered, independently-versioned crates, all
    sharing one workspace version:
    • uniko-store — Layer 1: graph storage, search, and the Locy runtime (the sole
      boundary to uni-db).
    • uniko-pipes — Layer 2: pipeline infrastructure (Step trait, circuit breaker,
      retry, dead-letter queue, metrics).
    • uniko-extract — Layer 3: content processing (NER, observations, chunking,
      ingest, embedding).
    • uniko-memory — Layer 4: memory management (pipelines, recall cascade, rules,
      consolidation).
    • uniko-cortex — Layer 5: higher reasoning (procedure promotion, topic detection).
    • uniko-api — the public facade: builders and re-exports, no logic.
  • Three prebuilt Python wheel variants, all importing the same uniko package
    (one installable at a time) — this lifts 0.1.0's "build from source" requirement:
    • uniko — CPU, statically-bundled ONNX Runtime. Self-contained, zero runtime
      configuration.
    • uniko-cuda — NVIDIA CUDA: ONNX Runtime CUDA execution provider plus
      on-GPU local LLM inference via mistralrs + candle CUDA kernels (Ampere baseline,
      forward-compatible to Ada/Hopper/Blackwell). Linux x86_64.
    • uniko-metal — Apple Silicon: ONNX Runtime CoreML plus mistralrs + candle
      Metal kernels for on-GPU local LLM. macOS arm64.
    • Wheels are abi3 for CPython ≥ 3.10 (one wheel per platform); GPU wheels resolve
      the host CUDA/cuDNN libraries at runtime rather than bundling them.
  • Single-sourced versioning. Every crate, the Python distribution version, and the
    runtime uniko.__version__ derive from one [workspace.package].version; CI guards
    the internal dependency pins and the wheel pyproject.toml files against drift.

Added — cognitive memory engine

  • Embedded store on uni-db. A single in-process graph database (graph + vector +
    full-text + Locy logic programming) backs the whole system; uniko-store is the only
    crate that touches it.
  • Typed knowledge-graph schema — 24 node types and 53 edge types, organized around
    communication: messages between participants are the atomic unit, and entities,
    observations, facts, procedures, and topics derive from them with full provenance.
  • Atomic, LLM-free ingest. Each message compiles into the graph in one
    all-or-nothing transaction, idempotent on message_id. CPU-side extraction runs
    first, with no LLM in the hot path:
    • local ONNX NLP cascade (kniv-deberta, INT8) producing POS / NER / SRL / DEP / CLS
      from a single encoder pass;
    • rule-based, code-AST, and ONNX named-entity recognition;
    • observation extraction (rules, a YAML rules engine, and SRL frames);
    • recursive text chunking;
    • embedding (BGE-small-en-v1.5, 384-d by default; pluggable) via uni-db auto-embed.
  • Entity-quality admission and canonicalization. Strict admission (on by default)
    drops NER noise — temporal, measurement, and quoted-string spans already captured as
    observations, plus greeting/discourse fragments mis-tagged as people — and gates the
    low-confidence catch-all. Canonical text normalization collapses case and punctuation
    variants ("Melanie." / "melanie" → melanie) so a name keys identically across
    rule-based and ONNX NER, ABOUT edges, and consolidation, with a cross-source dedup
    cascade over overlapping mentions.
  • Asynchronous consolidation, off the ingest hot path:
    • fact derivation from observation clusters (paraphrase collapsing), with
      SUPPORTED_BY edges weighted by Fact↔Observation cosine;
    • contradiction detection and entity-drift flagging over bitemporal (BTIC) intervals;
    • procedure promotion from repeated episode / action sequences;
    • topic detection via label-propagation communities (optional LLM naming, offline).
  • Three-phase recall cascade with coverage gating: Phase 1 over compiled knowledge
    (Facts / Topics / Procedures) → Phase 2 hybrid dense vector + BM25 over
    Episodes / Observations / Messages fused with Reciprocal-Rank Fusion (with optional
    temporal and graph / Personalized-PageRank channels) → Phase 3 full Chunk / Artifact
    fallback. Query understanding is local and rule-based; an optional cross-encoder
    reranker
    (MiniLM) rescores the fused results, and everything assembles into a
    token-budgeted context bundle.
  • Locy formal reasoning — database-native rule execution for derived knowledge; the
    stdlib sequence detector is the live engine.
  • Agent tools and a query feedback loop. Answered queries can be recorded as
    Episodes (question, answer, recall node IDs, coverage, usage), feeding procedure
    learning.
  • Visibility and access control by participant / team / org (public,
    private:{id}, team:{id}, org:{id}), applied to recall through a cached viewer
    policy.
  • Python SDK (alpha). import uniko exposes an async-first surface —
    Uniko → agent(id) → Agent → session(id) → Session, with verbs observe / recall /
    answer / query / ingest / forget, goals / tasks, and Locy logic — plus a blocking
    *_sync twin for every I/O call, a Pydantic IO layer, and a complete py.typed
    stub. Requires Python ≥ 3.10; runtime dependency pydantic ≥ 2.
  • Benchmark harness (uniko-bench) with LoCoMo and LongMemEval runners,
    microbenchmarks, and performance telemetry.

Benchmarks

Self-measured on the full LoCoMo10 set (10 conversations, 1,986 questions; 22-core
CPU + 8 GB consumer GPU). These are internal figures from the repo's own harness, not a
third-party leaderboard.

  • LLM-judge score 0.8117 (81.2%, Gemini-3.1); retrieval hit 0.8555 (85.6%);
    F1 0.321; total LLM cost (answer + judge) $3.55.
  • Ingest of 5,882 turns in 7.5 minutes at $0 (local ONNX only), ~76 ms/turn.
  • Mean Q&A latency 4.04 s (2.84 s recall + 1.20 s generation) — the fastest of six
    systems compared (vs Mem0, Graphiti, Cognee, RAG, and Full-Context), and 33–76×
    faster per-turn ingest than Graphiti / Cognee at $0 ingest cost.

Known limitations

  • No HTTP or MCP API. uniko ships as a Rust library (plus the Python SDK); there is
    no network surface or agent-facing tool server yet.
  • No CLI.
  • Python SDK is alpha. The full async surface ships with *_sync twins and typed
    stubs, but the API may still change before 1.0.
  • GPU wheels build the local-LLM stack from a pinned mistralrs/candle (crates.io is
    frozen at mistralrs 0.8.1); building them requires a CUDA (13.x) or Metal toolchain.
    The CPU uniko wheel needs neither.
  • Some recall paths are still improving. Date-anchored / temporal questions are the
    largest known failure category; retrieval tuning is ongoing.
  • Not yet shipped (design / roadmap only): HTTP/MCP server, CLI, sparse / ColBERT
    late-interaction retrieval, multimodal ingest, rule induction, and cross-agent
    sharing.

Installation

Rust:

[dependencies]
uniko-api = "0.1.1"

Python (CPU):

pip install uniko

GPU variants (install exactly one; all import as uniko):

pip install uniko-cuda    # NVIDIA CUDA, Linux x86_64
pip install uniko-metal   # Apple Silicon, macOS arm64