Mari documentation
Mari is a framework-neutral Python library for building knowledge systems from changing source material. It supplies immutable domain types, connector contracts, synchronization planning, multi-vector and graph retrieval, rank fusion, memory update plans, topic segmentation, evidence validation, freshness tracking, workflow reuse, trajectory analysis, and verification utilities.
How to read this page
“Current” sections describe importable code in mari_components. “Proposed” sections describe concrete interfaces and algorithms that are not released. Each research-derived mechanism links its evidence next to the explanation; library boundaries and failure behavior are labeled as Mari engineering contracts.
Install
Python 3.11–3.13 is supported. NumPy is the only runtime dependency.
pip install mari-components
# Optional runtime adapters
pip install 'mari-components[openai-agents]'
pip install 'mari-components[langchain]'How it works
The base wheel installs the domain, connector, synchronization, retrieval, knowledge, trajectory, and verification modules without a model SDK or database client. Extras add adapter imports only; applications still inject model calls, HTTP transport, persistence, clocks, and authorization decisions. This is package behavior, not a research-derived algorithm.
Architecture
Values cross explicit boundaries. Mari plans and validates; the application performs side effects.
| Mari supplies | Application supplies |
|---|---|
| Typed values and pure planning functions | Database and transactions |
| Connector polling and cursor contracts | Credentials, HTTP transport, retries, scheduler |
| Strict parsers for generated values | Model, prompts, and inference |
| Retrieval and index serialization | Embeddings and index lifecycle |
| Policy and evaluation functions | Authorization and agent runtime |
How it works
Provider data is normalized into immutable values. Pure functions transform those values into plans, candidates, reports, or index payloads. The caller validates the return value and commits it through its own transaction boundary. Because network and storage operations are injected, the same input values can be replayed in tests before any side effect occurs.
Documents, identity, and ACLs
KnowledgeDocument is the canonical provider-owned record. Its stable ID is {source_id}/{external_id}. Domain values are frozen dataclasses.
How it works
source_id names one configured source; external_id is the provider’s stable object key. Their pair prevents two providers from colliding. revision identifies content version, while updated_at is descriptive metadata and is never used as identity. ACL visibility and principals travel with the document so an allowed-ID set can be computed before retrieval scoring. Frozen values prevent an indexed object from changing behind its recorded revision.
from mari_components import DocumentACL, KnowledgeDocument, Principal
doc = KnowledgeDocument(
source_id="github:acme/product",
external_id="file:docs/refunds.md",
title="Refund policy",
body="## Enterprise\nRefunds close after 30 days.",
revision="8f31c2a", updated_at="2026-08-31T10:00:00Z",
source_url="https://github.com/acme/product/blob/main/docs/refunds.md",
acl=DocumentACL(visibility="restricted", principals=(
Principal(kind="team", identifier="support"),
)),
metadata={"path": "docs/refunds.md"},
)
assert doc.document_id == "github:acme/product/file:docs/refunds.md"PollPageUpserts, tombstones, cursor, checkpoint, snapshot completeness, provider metadata.
TombstoneExplicit deletion by source and external ID.
KnowledgeSectionStable section ID, offsets, text, and section revision.
EvidenceExact quote plus document, revision, span, and optional section coordinates.
W3C PROV: entity identity and revisionZanzibar: relationship-based authorization
Mari carries principals and visibility but does not implement an authorization engine. The application resolves those fields toallowed_document_ids.Polling and streaming connectors
Every connector defines a frozen configuration object, validation, and polling. GitHub, Slack, Google Drive, and Confluence also accept verified provider events. Network calls use an injected HttpTransport.
How it works
A polling connector starts from the caller’s cursor, requests bounded pages, normalizes provider objects, emits explicit tombstones, and returns the next cursor/checkpoint. A streaming connector verifies the raw delivery before parsing it, reduces provider payloads to bounded ChangeHint keys, coalesces duplicates, and canonically refetches the object. Both routes produce PollPage, so event order, partial webhook payloads, and provider retry behavior cannot bypass synchronization invariants.
Provider examples
All polling functions accept the same PollRequest and injected HttpTransport, and return an iterator of PollPage values.
GitHub
Files, issues, pull requests, and commits.
from mari_components.connectors import GitHubConfig, poll_github
cfg = GitHubConfig(token=token, repository="acme/product",
branch="main", paths=("docs/**",),
content_types=("files", "issues", "pull_requests"))
pages = poll_github(cfg, request, http=http)Slack
Channels, DMs, and canonical thread documents.
from mari_components.connectors import SlackConfig, poll_slack
cfg = SlackConfig(bot_token=bot_token,
history_token=history_token, channels=("C0123",))
pages = poll_slack(cfg, request, http=http)Google Drive
Drive files, Google Docs, changes, and push watches.
from mari_components.connectors import GoogleDriveConfig, poll_google_drive
cfg = GoogleDriveConfig(access_token=token, folder_id="folder-id")
pages = poll_google_drive(cfg, request, http=http)
# poll_google_drive_changes(...) and start_google_drive_watch(...)Confluence
Cloud pages converted from storage HTML to Markdown-like text.
from mari_components.connectors import ConfluenceConfig, poll_confluence
cfg = ConfluenceConfig(site_url="https://acme.atlassian.net/wiki",
email="bot@acme.com", api_token=token, space_key="ENG")
pages = poll_confluence(cfg, request, http=http)Dropbox
Native delta cursor with explicit deleted entries.
from mari_components.connectors import DropboxConfig, poll_dropbox
cfg = DropboxConfig(token=token, path="/Knowledge")
pages = poll_dropbox(cfg, request, http=http)Notion
Page search and bounded block-tree ingestion.
from mari_components.connectors import NotionConfig, poll_notion
cfg = NotionConfig(token=token)
pages = poll_notion(cfg, request, http=http)Airtable
Base metadata and table snapshots.
from mari_components.connectors import AirtableConfig, poll_airtable
cfg = AirtableConfig(token=token, base_id="appABC123")
pages = poll_airtable(cfg, request, http=http)Asana
Workspace or project tasks with offset checkpoints.
from mari_components.connectors import AsanaConfig, poll_asana
cfg = AsanaConfig(token=token, workspace_gid="workspace-gid",
project_gid="project-gid")
pages = poll_asana(cfg, request, http=http)Jira
Cloud issues with project or custom JQL scope.
from mari_components.connectors import JiraConfig, poll_jira
cfg = JiraConfig(site_url="https://acme.atlassian.net",
email="bot@acme.com", api_token=token, project_key="SUP")
pages = poll_jira(cfg, request, http=http)Linear
Issues and comments through the GraphQL API.
from mari_components.connectors import LinearConfig, poll_linear
cfg = LinearConfig(api_key=api_key, team_id="team-id")
pages = poll_linear(cfg, request, http=http)Trello
Open boards, lists, and cards.
from mari_components.connectors import TrelloConfig, poll_trello
cfg = TrelloConfig(api_key=api_key, token=token)
pages = poll_trello(cfg, request, http=http)Zendesk
Guide articles with ordered page checkpoints.
from mari_components.connectors import ZendeskConfig, poll_zendesk
cfg = ZendeskConfig(subdomain="acme",
email="bot@acme.com", api_token=token)
pages = poll_zendesk(cfg, request, http=http)from mari_components import PollRequest
from mari_components.connectors import GitHubConfig, poll_github, validate_github
config = GitHubConfig(token=token, repository="acme/product",
paths=("docs/**",), content_types=("files", "issues"))
validation = validate_github(config, http=http)
request = PollRequest(cursor=saved_cursor, page_size=100, page_limit=20)
for page in poll_github(config, request, http=http):
consume(page)Streaming
stream_pages requires a verifier, rejects oversized deliveries and batches, parses provider-specific hints, coalesces repeated aggregate keys, and calls an injected hydration function. The application owns the webhook server, queue, acknowledgement, and retries.
from mari_components.connectors import StreamEvent, stream_pages
event = StreamEvent(provider="slack", raw_body=raw_body, headers=headers)
def hydrate(hint):
document, complete = fetch_slack_thread_by_id(config,
hint.metadata["channel"], hint.metadata["thread_timestamp"], http=http)
return (PollPage(upserts=(document,) if document else (),
snapshot_complete=complete),)
for page in stream_pages((event,), verify=verify_signature, hydrate=hydrate):
consume(page)Connector-specific capabilities
- All twelve connectors: polling, validation, pagination limits, normalized documents, and explicit deletion handling.
- GitHub, Slack, Google Drive, and Confluence: verified streaming change hints plus canonical refetch.
- Slack: canonical thread fetch by ID.
- Google Drive: native Changes polling and push-watch registration.
- Confluence: direct canonical page fetch.
ConnectorDefinition.supports(ConnectorMode.POLL | STREAM)exposes mode capabilities for setup UIs.
OpenAPI: HTTP operation contractsCloudEvents: event envelopesRFC 2104: HMAC verification
Provider pagination, cursor, and signature schemes differ. Mari normalizes their observable results; it does not claim a universal delivery guarantee.Synchronization
plan_sync compares a durable SyncState with one PollPage and returns a side-effect-free SyncPlan. stream_sync applies the same rules across pages.
How it works
For each upsert, Mari validates source ownership and compares a deterministic content fingerprint with the manifest: equal means unchanged; unequal means upsert. Explicit tombstones always become deletes. Absence becomes deletion only after the terminal page of an authoritative full snapshot. The returned plan carries the prior generation as a compare-and-swap precondition and the next manifest/cursor as proposed state; persistence must atomically commit both data and state.
from mari_components import SyncMode
from mari_components.sync import SyncState, plan_sync
state = load_state() or SyncState()
for page in provider_pages:
plan = plan_sync(state, page,
source_id="github:acme/product", mode=SyncMode.FULL)
store.commit(upserts=plan.upserts, deletes=plan.deletes,
state=plan.state, expected_generation=plan.expected_generation)
state = plan.stateEnforced invariants
- Page replay is idempotent through content fingerprints and manifests.
- Only terminal, authoritative full pages reconcile absence.
- Explicit tombstones apply in full and incremental modes.
- Incomplete full sync cannot resume as incremental.
- Generation compare-and-swap prevents concurrent state loss.
- Foreign source IDs, duplicate IDs, and upsert/delete overlap are rejected.
Build Systems à la Carte: fingerprints and minimal rebuildsDynamo: versioning and reconciliation
Snapshot authority, deletion rules, and atomic compare-and-swap are Mari’s connector/store contract.Multi-vector retrieval
Mari implements MUVERA fixed-dimensional candidate generation, PolarQuant compression, and exact normalized MaxSim reranking in one retrieval path.
from mari_components.retrieval import FDEConfig, build_index, search_index
index = build_index({doc.document_id: token_vectors},
config=FDEConfig(repetitions=20, projection_dimension=16))
hits = search_index(index, query_token_vectors, limit=8,
allowed_document_ids=authorized_document_ids)serialize_index and deserialize_index use versioned, checksummed payloads. exact_maxsim is public for direct scoring.
How it works and backing algorithms
Mari's current path uses token-level late interaction: each query token takes its maximum similarity to any document token, and the maxima are summed. MUVERA maps those multi-vector sets to fixed-dimensional encodings for fast candidate generation; Mari then reranks the candidates with exact MaxSim. The packed Polar codec is an implementation-level compression of candidate encodings, not an alternative relevance model.
| Status | Index family | Representation and algorithm | Appropriate when | Primary source |
|---|---|---|---|---|
| Current | MUVERA + exact MaxSim | Multi-vector FDE candidate generation, compressed storage, exact late-interaction reranking | Fine-grained semantic matching where individual query terms matter | MUVERA · ColBERT |
| Proposed | Dense flat | Exact cosine, dot-product, or L2 scan over one vector per passage | Small corpora, evaluation baselines, or exact reproducibility | Dense Passage Retrieval |
| Proposed | HNSW | Hierarchical proximity graph for approximate nearest-neighbor search | Large mutable dense-vector collections with low-latency queries | HNSW |
| Proposed | IVF-PQ | Coarse inverted partitions plus product-quantized vector codes | Memory-constrained or very large dense indexes | Product Quantization · Faiss |
| Proposed | BM25 | Probabilistic lexical ranking over an inverted term index | Exact names, identifiers, code symbols, and domain terminology | BM25 and Beyond |
| Proposed | Learned sparse | Transformer-produced sparse term weights served by an inverted index | Lexical interpretability with learned expansion | SPLADE |
| Current | Rank fusion | Weighted reciprocal-rank fusion over independent result lists, with per-source contribution traces | Mixed corpora where source scores are not directly comparable | RAG-Fusion |
| Current | Graph propagation | Allowed-node personalized PageRank followed by weighted node-to-passage projection | Multi-hop recall from query-linked entities, facts, or sections | HippoRAG |
Proposed index interface
The common protocol should describe capabilities rather than a vendor. Index selection then becomes pipeline configuration and can be evaluated against recall, latency, memory, freshness, and ACL-filter behavior.
indexes = {
"exact": DenseFlatIndex(metric="cosine"),
"dense": HNSWIndex(metric="cosine", m=32, ef_search=128),
"compressed": IVFPQIndex(partitions=4096, subquantizers=32),
"lexical": BM25Index(k1=1.2, b=0.75),
"sparse": SparseVectorIndex(model="splade"),
"late": LateInteractionIndex(candidate="muvera", rerank="maxsim"),
}
hybrid = HybridIndex(arms=[indexes["lexical"], indexes["dense"], indexes["late"]],
fusion=ReciprocalRankFusion(k=60))Rank fusion, graph recall, and diverse packing
from mari_components.retrieval import (
maximal_marginal_relevance, personalized_pagerank,
project_graph_scores, reciprocal_rank_fusion,
)
fused = reciprocal_rank_fusion(
{"muvera": dense_ids, "lexical": lexical_ids, "recent": recent_ids},
weights={"recent": 0.25}, rank_constant=60,
eligible=authorized_document_ids.__contains__, limit=40)
nodes = personalized_pagerank(graph, query_seeds,
allowed_node_ids=authorized_graph_nodes, damping=0.85)
passages = project_graph_scores(nodes.hits, node_passages, limit=20)
context = maximal_marginal_relevance(
{hit.document_id: hit.score for hit in fused},
similarity=passage_similarity, relevance_weight=0.65, limit=12)
assert nodes.convergedMemory segmentation and mutation plans
hybrid_topic_segments splits a stream only where an attention-boundary peak and a semantic-similarity valley agree. The application extracts candidates from those bounded groups and classifies each one as add, update, delete, or no-op. plan_memory_mutations validates the decisions without writing storage.
How it works
Normalize boundary and adjacent-similarity arrays to the n−1 gaps between n turns. A gap is eligible only when its attention score is a local peak above the configured boundary threshold and its adjacent semantic similarity is below the valley threshold. Eligible gaps split consecutive, non-overlapping segments. Mutation planning then requires exactly one decision per candidate, validates update/delete targets against current IDs, rejects duplicate adds and conflicting operations on one target, and returns a deterministic plan.
from mari_components.knowledge import (
MemoryDecision, MemoryOperation, hybrid_topic_segments,
plan_memory_mutations,
)
segments = hybrid_topic_segments(turns,
attention_boundaries=attention, adjacent_similarities=similarity,
similarity_threshold=0.40)
plan = plan_memory_mutations(existing, candidates, {
"new-role": MemoryDecision(operation=MemoryOperation.UPDATE,
target_id="role", reason="newer explicit statement"),
"unchanged": MemoryDecision(operation=MemoryOperation.NOOP),
})
store.commit(plan, expected_generation=generation)Mem0: memory extraction and update operationsLightMem: topic-aware memory consolidation
The conjunctive peak/valley rule and mutation validation are Mari implementations; model-based classification remains outside the library.Knowledge parsers
Models return JSON-like values. Parsers resolve all evidence against supplied document and section revisions and return immutable typed values. Research establishes each task formulation; Mari implements a deterministic validation boundary rather than the cited model.
How it works
Each parser first requires the recipe’s top-level collection, then validates every required field and enum, resolves evidence through the exact contract below, derives deterministic audit signals, and constructs frozen result types. It never repairs a claim’s meaning. Batch claim assessment is the exception to fail-fast parsing: rows are keyed back to caller order, absent rows become uncertain, and individually malformed rows do not erase valid siblings.
| Parser | Produces | Research-backed task | Academic sources |
|---|---|---|---|
parse_facts | FactCandidate | Atomic claims and optional document-level relations with evidence | FActScore · DocRED |
parse_claim_assessments | FactAssessment | Supported, contradicted, or uncertain verdicts; decisive rows require evidence | FEVER |
parse_decisions | DecisionCandidate | Decision-related utterance extraction without treating topical language as proof | Hsueh & Moore · Karan et al. |
parse_answer | GroundedAnswer | Evidence-selected document QA, citations, or explicit insufficient evidence | QASPER · ALCE |
parse_answer_candidates | AnswerCandidate[] | Reusable question-answer pairs bound to supporting passages | QASPER |
parse_glossary | GlossaryCandidate[] | Term-definition relations, aliases, and source spans | DeftEval |
parse_digest | DigestSummary | Overall and topic summaries with separately inspectable evidence | QAGS · SummaC |
parse_impact | ImpactAssessment | In-scope affected-document proposals followed by deterministic dependency checks | Mari contract; no claimed benchmark reproduction |
parse_refinement | RefinementEdit[] | Bounded, attribution-aware, fact-preserving edit proposals | RARR · FactEditor |
from mari_components.knowledge import parse_answer
raw = model(question, documents)
answer = parse_answer(question, documents, raw)
print(answer.disposition) # grounded | insufficient_evidence
print(answer.grounding_coverage) # deterministic text coverage
print(answer.evidence[0].quote) # exact source textAdditional deterministic helpers include normalize_claim, deduplicate_fact_candidates, grounding_coverage, and excerpt. Recoverable batch drift is handled conservatively: assessment rows are restored to caller order, missing rows become uncertain, and good rows survive alongside invalid ones. Structured fact qualifiers preserve subject, relation, object, scope, validity, and conditions.
Evidence contracts
An evidence record is a byte-for-byte quotation bound to the exact document and section revision that was supplied to a parser. It is provenance, not a model confidence score.
How it works
- Restrict the corpus. Build an allowed map from only the
KnowledgeDocumentvalues passed by the caller. A model cannot cite an ID outside that map. - Resolve the document. Require
document_id. If exactly one allowed document contains the quote, Mari may recover a missing ID; zero or multiple holders is rejected. - Match exact text. Require a non-empty quote and test literal containment in the canonical document body. Fuzzy, normalized, or semantic matches are not accepted.
- Resolve one section. Split the document into current sections and find sections containing the quote. A repeated quote spanning multiple sections is rejected unless
section_idselects exactly one. - Derive coordinates. Mari computes
start = section.start + section.body.index(quote)andend = start + len(quote); it does not trust model-supplied offsets or revisions. - Bind revisions. The accepted record receives the current
document.revision, stablesection_id, and content-derivedsection.revision. These become the invalidation key.
Enterprise refunds close after 30 days.
+ unique section
document_id = …refunds.mdrevision = 8f31c2aquote = "30 days"start = 31 · end = 38section_id = enterprisesection_revision = sha256:…from mari_components.knowledge import parse_facts
raw = {"facts": [{
"claim": "Enterprise refunds close after 30 days.",
"evidence": [{"document_id": doc.document_id,
"section_id": "enterprise",
"quote": "30 days"}],
}]}
fact = parse_facts([doc], raw)[0]
e = fact.evidence[0]
assert doc.body[e.start:e.end] == e.quote
# Rejected: unknown document, absent quote, or a repeated quote
# whose section cannot be selected unambiguously.Dependency conversion
evidence_dependencies projects each record to (document_id, document_revision, section_id, section_revision), deduplicated by (document_id, section_id) and returned in stable order. Two records naming different revisions of the same key raise ValueError; silently choosing one would make reuse nondeterministic.
ALCE: citation correctness and completenessQASPER: evidence-bearing document QAFActScore: atomic factual claimsFEVER: evidence-backed verdictsW3C PROV: quotation, derivation, and revision
These works motivate inspectable evidence and revision provenance. Literal substring validation, unique-section resolution, and failure behavior are Mari engineering contracts.Freshness and impact
Freshness is an exact dependency comparison. It answers “did an input revision change?”—not “is the answer still semantically correct?”
How it works
- Record dependencies. Every derived artifact stores the document or section revision used to build it.
- Select comparison granularity. If a dependency names a section and the caller supplies a section-revision map, compare section hashes. Otherwise compare the containing document revision as a conservative fallback.
- Classify every key. Missing document/section →
missing; empty expected/current revision →unversioned; unequal revisions →stale; otherwise →current. - Reduce deterministically. Overall precedence is
missing > unversioned > stale > current. Changes and IDs are sorted, so the same inputs produce the same report. - Propagate impact.
impacted_artifactsevaluates each artifact independently and returns only non-current artifacts. Mari reports the set; the application chooses whether to regenerate, review, or retire them.
from mari_components.knowledge import (
FreshnessStatus, assess_dependencies, assess_freshness,
impacted_artifacts,
)
report = assess_freshness(answer.evidence, current_revisions,
current_section_revisions=current_sections)
if not report.reusable:
refresh(report.changes, report.missing_dependency_ids)
stale = impacted_artifacts(dependencies_by_artifact, current_revisions,
current_section_revisions=current_sections)Document edit versus affected section
# The document changed v1 → v2, but the cited section is still s1.
current_revisions = {doc_id: "v2"}
current_sections = {(doc_id, "refund-window"): "s1"}
fine = assess_dependencies(deps, current_revisions,
current_section_revisions=current_sections)
assert fine.status == FreshnessStatus.CURRENT
coarse = assess_dependencies(deps, current_revisions)
assert coarse.status == FreshnessStatus.STALE # safe fallbackBuild Systems à la Carte: dependency-driven recomputationRAG: updateable non-parametric knowledge and provenanceW3C PROV: revision and derivation
Mari applies build-system invalidation to knowledge artifacts. Status precedence, section fallback, and reuse policy are explicit Mari contracts, not semantic change detection.Sections and incremental fact scans
document_sections maps Markdown headings to stable section IDs and content revisions. section_revisions builds the current revision map. Fact scans can then skip unchanged sections.
How it works
Scan Markdown heading lines, treating content before the first heading as a preamble. Normalize each heading into a slug and suffix collisions deterministically. Store absolute body offsets and hash the section body into its revision. pending_fact_sections compares (document_id, section_id) → revision with the last committed scan and yields new or changed sections only. Persist new scan revisions only after extracted facts commit, or a failed run would incorrectly suppress retry.
from mari_components.knowledge import (
document_sections, fact_scan_revisions, pending_fact_sections,
)
sections = document_sections(document)
pending = pending_fact_sections([document], previous_scan_revisions)
facts = [parse_facts([document], model(section.body)) for section in pending]
next_revisions = fact_scan_revisions(pending) # persist only after facts commitBuild Systems à la Carte: change detection and recomputationRFC 6920: digest-based content identity
Markdown heading segmentation and slug collision rules are Mari engineering contracts.Reviewed workflows and cached answers
Reviewed workflow indexes match new requests to approved intents. Policy thresholds independently control speculative retrieval and direct cached-response reuse.
How it works
Build an index from reviewed workflow intent vectors. At query time, compute normalized similarity, retain only workflows whose dependencies are authorized, and choose the best match with stable ties. Crossing the lower threshold may start retrieval speculatively; crossing the higher threshold only makes reuse eligible. A cached response is returned only after its exact evidence dependencies pass freshness checks. Similarity never overrides ACL or revision failure.
from mari_components.trajectories import (
WorkflowPolicy, build_reviewed_workflow_index, decide_reviewed_workflow,
)
index = build_reviewed_workflow_index(reviewed_workflows)
decision = decide_reviewed_workflow(query_vectors, index, current_revisions,
policy=WorkflowPolicy(speculation_threshold=0.72, cache_threshold=0.95),
allowed_document_ids=authorized_document_ids)Related APIs: match_reviewed_workflow, start_speculative_retrieval, match_cached_response, and workflow_freshness. Reuse requires a strong match plus fresh, authorized dependencies.
GPTCache: semantic caching for language-model queriesBuild Systems à la Carte: dependency-valid reuse
The two thresholds, authorization gate, and exact freshness condition are Mari policy.Trajectories and agent evaluation
normalize_steps converts runtime records into privacy-bounded TrajectoryStep values. parse_trajectory_analysis validates model-proposed phases. Mari provides adapters, not an agent loop.
How it works
Adapters map framework events into ordered AgentEvent values. Normalization assigns stable step positions, keeps allowlisted metadata, and redacts sensitive argument names and transport fields. Tool evaluation compares observed names and counts against expectations; outcome evaluation compares terminal paths and completion. A proposed phase analysis must cover every observed event exactly once with contiguous, non-overlapping ranges and known tool families.
from mari_components.agents import evaluate_outcome, evaluate_tools
from mari_components.trajectories import normalize_steps
steps = normalize_steps(runtime_events)
tools = evaluate_tools(events, expected_tools=("search_knowledge",))
outcome = evaluate_outcome(paths=("resolved",),
expected_paths=("resolved",), completed=True)AgentEvent and EventKind are framework-neutral. Optional adapters cover OpenAI Agents and LangChain/LangGraph. Normalization redacts common sensitive arguments; phase validation requires the returned ranges to cover observed events exactly.
from mari_components.trajectories import parse_trajectory_analysis
analysis = parse_trajectory_analysis(normalized_events, model_labels,
family_map={"search_product_knowledge": "inspect",
"answer": "answer"})AgentBench: multi-environment agent evaluationOpenTelemetry trace specification
Mari’s event schema, redaction list, exact phase coverage, and outcome predicates are library contracts.Verification portfolios
Verification functions score already-parsed values and retain all successful attempts and failures for audit.
How it works
best_of_n calls the producer up to n times, parses each output, records parse failures without discarding successful siblings, scores valid candidates, and selects the highest score with stable first-wins ties; it may stop once the threshold is met. verdict_consensus counts typed verdicts rather than free text. Grounding scores combine declared deterministic components; they are used for ranking or abstention, never calibrated as probabilities.
from mari_components.verification import best_of_n, score_grounded
result = best_of_n(
lambda: model(question, documents),
lambda raw: parse_answer(question, documents, raw),
lambda answer: score_grounded(answer,
required_ideas=("eligibility", "time limit")),
attempts=3, threshold=0.90)
audit(result.selected, result.attempts, result.failures, result.stopped_early)select_bestScores existing candidates with stable tie-breaking.
verdict_consensusAggregates supported, contradicted, and uncertain assessments.
score_groundedEvidence, coverage, completeness, corroboration, certainty.
harmonic_scoreidea_completenessSelf-consistency improves chain-of-thought reasoningFEVER: evidence-based verificationALCE: citation quality evaluation
Mari exposes an auditable selection portfolio; it does not reproduce model sampling or benchmark metrics.Errors and deliberate boundaries
How it works
Exceptions classify which boundary failed and whether repeating the same request can help. Connector adapters translate provider responses into authentication, transient, or permanent failures. Snapshot validation raises IncompleteSnapshot before absence can become deletion. Knowledge parsers raise MalformedModelOutput before an invalid value crosses into typed state. Mari never retries automatically because retry budgets, clocks, credentials, and side effects belong to the host.
| Error | Meaning | Typical handling |
|---|---|---|
AuthenticationFailure | Credentials rejected | Request new credentials |
TransientFailure | Temporary provider failure | Retry using app policy |
PermanentFailure | Request cannot succeed unchanged | Require intervention |
IncompleteSnapshot | Listing is not authoritative | Do not infer deletion |
MalformedModelOutput | Generated value violates parser contract | Retry or abstain |
Safe representations and connector contracts
Credentials are excluded from connector configuration representations. HttpRequest representations redact authorization headers, bodies, URL userinfo, and common sensitive query parameters. check_connector_contract provides an executable contract check for third-party connector implementations.
from mari_components import SyncMode
from mari_components.testing import check_connector_contract
pages = tuple(my_connector(config, request, http=fake_http))
report = check_connector_contract(pages, mode=SyncMode.FULL,
starting_cursor=request.cursor)
assert report.pages == len(pages)Not included: model client, prompt framework, database, scheduler, credential store, authorization engine, agent runtime, or worker queue.
Hypothetical and hierarchical retrieval
These functions construct alternative query representations and bounded navigation structures. Generation, encoding, clustering, summarization, and relevance models are injected; Mari owns shape validation, deterministic IDs, budgets, and traces.
How it works
hypothetical_document_embedding weights and averages caller-encoded hypothetical answers, then L2-normalizes the vector used for retrieval; generated text is never stored as fact. build_summary_tree repeatedly validates a caller-proposed partition of every current root, creates stable parent nodes, and stops when the root count no longer decreases. walk_summary_tree expands the highest-scoring children under explicit branch and visit budgets and returns visited paths plus exhaustion state.
HyDE: hypothetical document embeddingsRAPTOR: recursive summary treesMemWalker: bounded memory-tree navigation
Paper-derived retrieval construction
from mari_components.retrieval import (
build_summary_tree, hypothetical_document_embedding, walk_summary_tree,
)
hyde_vector = hypothetical_document_embedding([
document_encoder(text) for text in generate_hypotheses(query)
])
tree = build_summary_tree(section_text_by_id,
cluster=lambda nodes, level: cluster_embeddings(nodes, level),
summarize=lambda children, level: summarize(children, level))
walk = walk_summary_tree(tree, lambda node: similarity(query, node.text),
branch_factor=2, max_visits=24)
sections = document_store.get_many(walk.leaf_ids)Adaptive retrieval and compression
Retrieval can be triggered, corrected, rescored, or compressed at explicit decision points instead of running as one opaque model call.
How it works
CRAG routing maps evaluator scores through two thresholds to use the corpus, augment it with external search, or replace it. FLARE finds low-confidence tokens in a predicted future sentence, removes those tokens, and uses the remaining text as a retrieval query before regeneration. Self-RAG combines generation, retrieval, relevance, support, and utility signals with visible weights. RECOMP selects scored sentences under a token budget, then restores their source order.
Self-RAG: reflection-token scoringCRAG: corrective retrievalFLARE: forward-looking active retrievalRECOMP: selective compression
from mari_components.retrieval import (
CompressionSentence, plan_active_retrieval, plan_corrective_retrieval,
selective_compression,
)
from mari_components.verification import score_self_rag_candidate
correction = plan_corrective_retrieval(retrieval_evaluator(query, hits),
lower_threshold=-0.8, upper_threshold=0.6)
active = plan_active_retrieval(future.tokens, future.probabilities, threshold=0.2)
reflection = score_self_rag_candidate(generation_probability=signals.generation,
retrieve_probability=signals.retrieve, relevance_probability=signals.relevant,
support_probability=signals.supported, utility=signals.utility)
compressed = selective_compression([
CompressionSentence(sentence_id=s.id, text=s.text, token_count=count_tokens(s.text),
relevance=compressor.score(query, s.text)) for s in sentences
], token_budget=600, relevance_threshold=0.4)Memory organization and evidence notes
These functions link related notes, rank memories for recall, and decide whether retrieved evidence can support an answer.
How it works
Note evolution applies a link threshold and a stricter metadata-evolution threshold to caller-supplied similarities. Salience exponentially decays recency, min-max normalizes recency, importance, and relevance over the candidate set, then returns every weighted contribution. Evidence-note decisions validate per-document relevance and answer support before choosing retrieved evidence, explicitly allowed parametric knowledge, or unknown.
A-MEM: dynamic note evolutionGenerative Agents: recency, importance, and relevanceChain-of-Note: sequential evidence decisions
from mari_components.knowledge import (
MemorySignal, plan_note_evolution, rank_salient_memories,
)
from mari_components.verification import (
EvidenceNote, decide_from_evidence_notes,
)
evolution = plan_note_evolution(new_note.id, similarity_by_note_id,
link_threshold=0.72, evolution_threshold=0.91)
salient = rank_salient_memories([
MemorySignal(memory_id=m.id, hours_since_access=hours_since(m.last_accessed),
importance=importance(m), relevance=relevance(query, m)) for m in memories
], recency_decay=0.995, limit=20)
decision = decide_from_evidence_notes([
EvidenceNote(document_id=n.document_id, relevant=n.relevant,
supports_answer=n.supports_answer) for n in model_notes
])Knowledge admission and mutation planning
Admission is evaluated before reconciliation. A candidate may be valid JSON and still be unsafe, low-authority, redundant, or unsupported. Reconciliation runs only for accepted candidates.
How it works
Run provenance, evidence-span, recalled-input, secret, external-instruction, authority, and confidence rules over the candidate. Aggregate rule results into ACCEPT, DEFER, REJECT, or QUARANTINE with reason codes. Only accepted candidates reach mutation reconciliation, which validates add, merge, supersede, retract, or unchanged operations against the current canonical slot without writing storage.
Indirect prompt injectionW3C PROVMem0: memory mutation operations
Disposition precedence and commit boundaries are proposed Mari contracts.candidate = extractor.propose(observation)
admission = admit(candidate,
rules=[RequireEvidenceSpan(), RejectRecalledInput(), QuarantineSecrets(),
QuarantineExternalInstructions(), EnforceSourceAuthority()],
thresholds=AdmissionThresholds(accept=0.90, defer=0.65))
match admission.disposition:
case ACCEPT:
mutation = reconcile(candidate, current=artifact_store.canonical_slot(candidate))
# ADD | MERGE | SUPERSEDE | RETRACT | UNCHANGED
case DEFER: review_queue.put(candidate, admission.reasons)
case QUARANTINE: quarantine.put(candidate, admission.reasons)
case REJECT: audit.record(admission)Entity resolution with explicit uncertainty
The cascade spends expensive work only after cheap deterministic checks. It never converts an ambiguous candidate into a merge without a configured threshold or review decision.
How it works
Block candidates by tenant, scope, and entity type; compare normalized exact aliases; calculate field-agreement and fuzzy scores; retrieve a small embedding neighborhood only for unresolved candidates; then apply separate link and review thresholds. Scores above link become a proposed canonical ID, scores in the review band retain all candidates and their feature trace, and lower scores remain distinct entities.
resolver = EntityResolver([
ScopeAndTypeBlock(), NormalizedAliasMatch(),
ProbabilisticFieldMatch(link=0.95, review=0.72),
EmbeddingCandidates(index=entity_index, limit=10),
])
resolution = resolver.resolve(candidate, scope=artifact.scope)
if resolution.ambiguous:
review_queue.put(resolution.candidates, resolution.comparison_trace)Graph recall and corpus aggregation
Passage retrieval and corpus summarization are different operations. Personalized PageRank is a current bounded multi-hop recall function. Leiden communities and map-reduce reports are proposed, separately versioned aggregation stages.
How it works
Link query mentions to authorized seed nodes, induce an allowed subgraph, and propagate Personalized PageRank mass until tolerance or iteration limits; project node mass back to evidence-bearing sections. Separately, Leiden partitions the graph into well-connected communities, recursive grouping forms levels, and evidence-linked community reports support global map-reduce queries. Local queries fan out from entities; drift queries start globally and open bounded local branches.
HippoRAG: Personalized PageRank recallLeiden community detectionGraphRAG: community reports and global query
recall = PersonalizedPageRank(
seeds=link_query(query, entities, facts), damping=0.50, hops=3,
edge_filter=AuthorizedAt(scope=user.scope, at=query.time),
project_to="source_sections")
communities = HierarchicalLeiden(resolution=1.0, max_size=40).fit(graph)
reports = summarize_communities(communities, evidence_policy=ExactEvidence())
answer = query_corpus(query, mode="global", reports=reports,
reduction=RatedMapReduce(max_partial_answers=24))Tiered memory consolidation
Tiers are policies over cost and lifecycle, not hard-coded stores. Topic segmentation is current; compression, promotion, and offline scheduling are proposed. Each promotion creates a dependency-bearing proposal, and raw observations remain available for audit.
How it works
Filter observations cheaply, group them at attention-peak/similarity-valley topic boundaries, compress within bounded groups, and score promotion from recurrence, recency, usefulness, and evidence diversity. Expensive resolving, superseding, and summarization run in an offline call/token budget. Promotion creates a new artifact revision linked to every contributing observation.
policy = ConsolidationPolicy(
segment=TopicBoundary(window=12, threshold=0.68),
promote=PromotionScore(recurrence=0.30, recency=0.15,
usefulness=0.35, evidence_diversity=0.20),
schedule=OfflineWindow(max_model_calls=20, max_tokens=50000))
plan = plan_consolidation(observations, policy=policy)
commit(review(plan.mutations), dependencies=plan.dependencies)Event sourcing and disposable projections
Canonical artifacts and append-only events remain authoritative. Search indexes, backlink tables, graph views, digests, and human-readable logs are projections with explicit build identities and safe swap semantics.
How it works
Append a validated event with an expected generation, fold ordered events through a deterministic projector into a staging version, and record schema plus embedding/configuration fingerprints. Validate counts, checksums, scope isolation, and sample queries before atomically switching the current pointer. A failed replay never replaces the last valid projection, and retaining the prior pointer makes rollback constant-time.
events.append(KnowledgeEvent(kind="artifact.superseded", payload=mutation,
actor=reviewer, expected_generation=41))
build = rebuild_projection(events, projector=SearchProjector(
embedding_identity=embedder.fingerprint(), schema_version="3"))
validate(build, checks=[DocumentCount(), Checksums(), ScopeIsolation(), SampleQueries()])
projections.atomic_swap(build, retain_previous=True)
# A failed build never replaces the last valid read version.Procedural learning and regression gates
Execution feedback can propose procedural updates, but promotion depends on held-out cases, negative outcomes, cross-procedure interference, and explicit review.
How it works
Reflect over a trajectory and observed outcome, turn the reflection into atomic add/update/tag/remove operations, and apply the patch to a copy of the active skillbook. Evaluate that candidate on its own cases, related-procedure cases, and known failures. Hard gates reject grounding or ACL regressions; passing creates a review proposal rather than activating it automatically. Worked, failed, and partial attempts remain separately retrievable.
Agentic Context Engineering: incremental skillbooksReflexion: learning from verbal feedbackLongMemEval: long-horizon memory evaluation
reflection = reflect(trajectory, outcome=outcome, evidence=observed_context)
patch = curate(reflection, operations={"ADD", "UPDATE", "TAG", "REMOVE"})
candidate = skillbook.apply_to_copy(patch)
gate = regression_gate(candidate, baseline=skillbook.active,
suites=[own_cases, related_procedure_cases, known_failures],
require={TaskSuccess(): NoRegression(), Groundedness(): AtLeast(0.95),
ACLLeakage(): Exactly(0.0)})
if gate.passed:
skillbook.propose(candidate, trace=gate.trace) # human promotion remains explicitLong-horizon memory evaluation
A memory system needs separate measurements for writing, updating, retrieving, temporal reasoning, abstaining, and respecting context limits. One aggregate answer score hides which subsystem failed.
How it works
Freeze conversations, source revisions, expected evidence, and change events into cases. Replay each case under several corpus sizes and context budgets. Score extraction at write time, evidence recall at query time, cross-session synthesis, ordering and date reasoning, correction after source updates, abstention when evidence is absent, and ACL leakage. Store per-stage traces with every score so a regression points to the responsible parser, index, policy, or packing decision.
report = evaluate_memory_system(system, cases,
corpus_sizes=(100, 10_000, 1_000_000),
context_budgets=(2_000, 8_000, 32_000),
metrics=[ExtractionF1(), EvidenceRecall(k=10), UpdateFidelity(),
TemporalAccuracy(), AbstentionPrecision(), ACLLeakage()])
report.by_capability["updates"]
report.failures # case, stage, revisions, trace, observed outputUnified artifact model
A generic envelope would give facts, answers, decisions, summaries, procedures, and graph statements common identity, scope, provenance, review, temporal, and supersession semantics.
How it works
The payload type T holds domain content; the envelope holds governance. Artifact identity stays stable while each revision is immutable. Evidence and derived_from capture inputs, generated_by captures the producing activity/configuration, validity bounds describe when the claim applies, and supersedes closes a lineage edge without erasing history. Stores reject a revision if its evidence, scope, or predecessor is invalid.
artifact = KnowledgeArtifact[PolicyFact](
id="fact:refund-window:enterprise", revision="sha256:8f31…",
value=PolicyFact(days=30),
scope=KnowledgeScope(tenant="acme", space="support"),
evidence=answer.evidence, valid_from="2026-01-01", valid_to=None,
recorded_at=clock.now(), review_state=ReviewState.APPROVED,
generated_by=Activity("refund-policy/v4", model="extractor@2026-08"),
derived_from=("github:policy/refunds.md@8f31c2a",),
supersedes=("fact:refund-window:enterprise@v2",))Storage protocols and conformance
Capability protocols would allow independent document, artifact, vector, lexical, and graph implementations.
How it works
Protocols specify observable behavior rather than backend classes. A store implementation declares capabilities, then runs a shared conformance suite against replay, atomic revision, isolation, deletion, deterministic ordering, and point-in-time cases. Cross-store operations use an application transaction/outbox boundary; Mari does not pretend separate databases share an atomic commit. Indexes remain disposable projections that can be rebuilt from documents and artifacts.
class DocumentStore(Protocol):
def commit_sync(self, plan: SyncPlan) -> None: ...
def get_many(self, ids: Iterable[str]) -> Sequence[KnowledgeDocument]: ...
class ArtifactStore(Protocol):
def apply(self, mutation: ArtifactMutation) -> None: ...
def at_time(self, id: str, when: datetime) -> KnowledgeArtifact | None: ...
class VectorIndex(Protocol): ...
class LexicalIndex(Protocol): ...
class GraphIndex(Protocol): ...Conformance tests would cover replay safety, deterministic ordering, point-in-time reads, tenant isolation, atomic revisions, and delete behavior.
Typed knowledge pipelines
Composable stages would transform typed inputs and emit reviewable ArtifactMutation values: create, revise, supersede, retract, or leave unchanged.
How it works
Each stage declares input/output types, a versioned configuration fingerprint, and whether it is pure or calls an injected service. The runner topologically orders stages, passes immutable batches, records input revisions and stage results, and stops dependent stages after failure. Outputs are mutation proposals; a final policy validates evidence, scope, and expected artifact revision before the application commits them.
pipeline = Pipeline[KnowledgeDocument, KnowledgeArtifact](
extract(FactExtractor(model=model)), resolve(EntityResolver(catalog=entities)),
link(EvidenceLinker()), review(ReviewPolicy(min_corroboration=2)),
index(vector=vector_index, graph=graph_index))
result = pipeline.run(changed_documents)
artifact_store.apply(result.mutations)
trace_store.write(result.trace)Retrieval plans and context envelopes
A retrieval plan would run explicit arms, fuse ranks, enforce scope and freshness, rerank, and pack a bounded context envelope. Its trace explains inclusion and exclusion.
How it works
Run semantic, lexical, graph, and recency arms over authorized IDs. Convert arm scores to ranks and combine them with reciprocal-rank fusion, then discard stale dependencies, rerank survivors, diversify near-duplicates, and greedily pack whole evidence excerpts under token/document limits. The envelope contains rendered context plus source revisions and per-candidate include/exclude reasons, allowing the caller to reproduce what the model saw.
plan = RetrievalPlan(arms=[Semantic(vector_index, limit=40),
Lexical(lexical_index, limit=30), GraphExpand(graph_index, hops=2),
RecentChanges(window="14d")], fusion=ReciprocalRankFusion(k=60),
reranker=ExactMaxSim())
context = assemble_context(query, plan=plan, scope=user.knowledge_scope,
budget=ContextBudget(tokens=6000, documents=12))
model(context.render())
audit(context.retrieval_trace)Bi-temporal knowledge graph
Statements would track valid time (when the claim applied) and transaction time (when the system learned it), supporting historical queries and late corrections.
How it works
An assertion is append-only and carries two intervals. A correction learned today may close an older assertion’s transaction interval while preserving its historical valid interval. Query at filters valid time; known_at filters transaction time; both must contain their requested timestamp. Contradictions create explicit edges or superseding revisions instead of destructive overwrites.
graph.assert_fact(subject="plan:enterprise", predicate="refund_window_days",
object=30, valid_time=Interval("2026-01-01", "2026-08-31"),
transaction_time=clock.now(), evidence=evidence)
then = graph.query(at="2026-06-01", known_at="2026-09-01")
now = graph.query(at=clock.now(), known_at=clock.now())Procedural knowledge
Successful trajectories could produce versioned procedure candidates. Regression gates and human review would separate observed behavior from active behavior.
How it works
Cluster successful traces by reviewed intent, extract a parameterized tool/action sequence with preconditions and failure exits, and retain links to the source traces. Replay the candidate on held-out cases, compare task success, tool correctness, grounding, cost, and regressions with the active version, then produce a review proposal. Only an explicit application commit can activate a version; failed attempts remain negative evidence.
candidate = learn_procedure(successful_runs, intent="process enterprise refund")
report = evaluate_procedure(candidate, cases=refund_regression_suite,
metrics=[TaskSuccess(), ToolCorrectness(), Groundedness(), Cost()])
if report.passes_gates:
procedures.propose(candidate, report=report) # review still requiredEvaluation and compilation
A compiler would search pipeline and retrieval configurations against knowledge-system objectives and return a reviewable, versioned configuration proposal.
How it works
Declare tunable parameters, hard constraints, and optimization metrics. For each candidate configuration, run the same frozen training cases, cache stage results by configuration/input fingerprints, reject any candidate that violates provenance, update fidelity, or ACL constraints, and rank feasible candidates on grounded recall, cost, and latency. Validate the selected configuration once on held-out cases; compilation returns a report and proposal, never a deployment side effect.
compiled = compile_knowledge_system(pipeline, trainset=train_cases,
validation=validation_cases, # optimizer may inspect these; never test_cases
objectives={GroundedRecall(): Maximize(), ProvenanceAccuracy(): Require(1.0),
UpdateFidelity(): Require(1.0), ACLLeakage(): Require(0.0),
ContextTokens(): Minimize(), LatencyP95(): Minimize()},
search_space=KnowledgeConfigSpace())
review(compiled.config, compiled.report, compiled.failures)
test_report = evaluate_once(compiled.config, cases=test_cases)
deploy(compiled.config, evidence=test_report) # explicit application action