Repository navigation
Releases
v0.3.0
[0.3.0] - 2026-09-26
Breaking
-
ModalityExtractor::extractis replaced byprepare+apply_in_tx,
with a new defaultedfinish_post_commit. Attachments now commit inside the
same transaction as the turns carrying them, which requires uniko to own that
transaction. Atxparameter onextractwould not be enough: a unit must
know everycontent_idit 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
extractwould
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 theimplblock 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::summarizereturnsSummarizeReportrather 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::revisiondeclare 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 withIdConflict, because
a revision id is a promise about the content.A new revision of a source stamps
superseded_aton the previous one and
recordsSUPERSEDESfrom new to old.Agent::retire_sourcestamps
Source.retired_at. Ordinary recall excludes both, so replaced or
retired evidence cannot ground a current answer, while
Scope::include_supersededreaches 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:ArtifactContentrow but not a:Source.SUPERSEDESwas already declaredRule → 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:Sourcenode holds the logical
identity, withFROM_SOURCEfrom the Message or Artifact;categoryand
source_idare 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 thecoveragescore 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. EveryRecallItem
reports its owncategoryandsource_id;Nonemeans "none recorded" or
"no single traceable source", not "filtered out".Episodecarries 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.ArtifactandFactbecome 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 separateobservecalls
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::observeis 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 itsNEXT
edge. -
Agent::finalize_sessionquiesces 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. SinceFinalizeReport::rebuiltORs 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_rowsdropped 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_CONTENTedge, 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 of2*pages + 4*blocks + chunks
separate commits into a single one. -
PDF provenance edges could vanish silently.
link_pdf_contextskipped an
unresolvable reference with no output at all — not even the debug line the
artifact path emits — wroteATTACHED_TO...
v0.2.1
What's Changed
- Enhance entity handling and recall mechanisms in storage by @milliondreams in #33
- deps: bump workspace version to 0.2.1 by @milliondreams in #34
Full Changelog: v0.2.0...v0.2.1
What's Changed
- Enhance entity handling and recall mechanisms in storage by @milliondreams in #33
- deps: bump workspace version to 0.2.1 by @milliondreams in #34
Full Changelog: v0.2.0...v0.2.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
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
Full Changelog: v0.1.1...v0.2.0
v0.1.1
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 touni-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
unikopackage
(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
abi3for 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
runtimeuniko.__version__derive from one[workspace.package].version; CI guards
the internal dependency pins and the wheelpyproject.tomlfiles 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-storeis 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 onmessage_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-dbauto-embed.
- local ONNX NLP cascade (
- 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,ABOUTedges, 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_BYedges 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).
- fact derivation from observation clusters (paraphrase collapsing), with
- 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 unikoexposes 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
*_synctwin for every I/O call, a Pydantic IO layer, and a completepy.typed
stub. Requires Python ≥ 3.10; runtime dependencypydantic ≥ 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
*_synctwins 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 CPUunikowheel 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 unikoGPU 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