Integrate

Remaind is a substrate, not a framework — it does not run your agent or call your model. It is the durable memory your agent loop reads and writes. You own the loop; Remaind owns the .context/ directory. Everything below uses the stable public API.

Install

pip install remaind            # core: rule-based compaction
pip install 'remaind[llm]'     # + the Anthropic-backed LLM compactor

The [llm] extra is only needed for LLMCompactor. The core install — and CallableCompactor, which works with any local model — needs no extra.

The public API

import remaind is the entire stable surface. Everything in remaind.__all__ is stable; the underscore-prefixed modules are internal — do not import from them.

import remaind

remaind.__all__        # the entire stable public surface
remaind.__version__

1 · Initialize — once per project

import remaind

base = "./my-agent-run"          # the directory that will hold .context/
remaind.init(base)               # creates base/.context/  (InitError if it exists)

# Every event needs a session_id + task_id — generated by init, read once:
state = remaind.status(base)
session_id = state["session_id"]
task_id = state["task_id"]

2 · Log events as the agent works

Open one EventWriter for the run and append an event for every meaningful thing that happens. Two things are automatic: redaction (secrets are hashed before the event touches disk) and large-output routing (oversized or binary content goes to artifacts/ with excerpts + a hash kept in the event).

writer = remaind.EventWriter.open(base)

writer.append(remaind.EventInput(
    type="user_message",
    actor="user",
    summary="Asked to refactor the auth module",   # short — for handovers & search
    session_id=session_id,
    task_id=task_id,
    content="Please refactor src/auth/ to use the new token format...",
    importance=3,                                  # a critical user instruction
))

Event types: user_message, assistant_message, tool_call, tool_result, command_output, file_change, decision, error. (compaction, validation, resume, rollback, init are written by Remaind itself.)

Importance drives what compaction preserves — tag it honestly. 0 trace · 1 normal · 2 decision / file change / blocker / result (always preserved) · 3 critical user instruction / active next step (explicit in state or handover). See the Docs for the full table.

You can also keep the derived state current as work progresses — update_state validates, recomputes the threshold band, and writes atomically with a history snapshot:

remaind.update_state(base, lambda st: {
    **st,
    "next_step": "wire the new token format into the middleware",
    "decisions": st["decisions"] + ["token format v2"],   # lists are append-only
})

3 · Track the context budget

Remaind does not see your real model window — you feed it an estimate; it tells you which band you are in.

tokens = remaind.estimate_tokens(whole_context_so_far)
remaind.update_state(base, lambda st: {**st, "estimated_context_tokens": tokens})

cs = remaind.compaction_status(base)
cs.band              # "normal" | "warning" | "hard" | "emergency"
cs.recommendation    # human-readable next action
cs.compaction_needed # band != "normal"
cs.compaction_urgent # band is "hard" or "emergency"

4 · Compact when the band climbs

A compactor only proposes a candidate; a structured validator decides whether to accept it. If the candidate drops a decision, ungrounds next_step, or fails to represent an importance = 3 item, it is rejected and the prior handover is kept untouched. A weak model cannot corrupt context.

try:
    result = remaind.compact(base)
    print(f"handover {result.handover_chars_before} -> {result.handover_chars_after} chars")
except remaind.CompactionRejected as exc:
    # The candidate was unfaithful. Nothing lost — the prior handover stands.
    # A validation event records exactly why.
    print("rejected:", exc.validation["missing_items"])
except remaind.CompactionError as exc:
    print("compaction could not run:", exc)

5 · Resume into a fresh context

When you start a fresh model context — a new run, or after hitting the window limit — build a resume packet and inject it. Injecting it is your job: Remaind assembles the packet; your harness decides where it goes.

result = remaind.resume(base)
fresh_context = result.packet.content     # markdown — assembled for a fresh agent

# Injecting the packet is YOUR job — typically prepended to the new
# context's system prompt or sent as its first message:
start_model_session(system_prompt=BASE_PROMPT + "\n\n" + fresh_context)

gate = result.packet.gate
if gate.should_interrupt:
    for concern in gate.concerns:        # conflict / missing next_step /
        print("RESUME GATE:", concern)   # unrepresented instruction / risky tool

A complete minimal agent loop

import remaind

base = "./my-agent-run"
remaind.init(base)

state = remaind.status(base)
sid, tid = state["session_id"], state["task_id"]
writer = remaind.EventWriter.open(base)

def log(type_, actor, summary, *, content=None, importance=1):
    writer.append(remaind.EventInput(
        type=type_, actor=actor, summary=summary,
        session_id=sid, task_id=tid, content=content, importance=importance,
    ))

# Wire a compactor once — swap for CallableCompactor to use a local model.
compactor = remaind.LLMCompactor.from_context(base)

while not done:
    user_msg = get_user_message()
    log("user_message", "user", user_msg[:80], content=user_msg, importance=3)

    reply, tool_calls, total_tokens = run_model_turn(...)
    log("assistant_message", "assistant", reply[:80], content=reply)
    for call in tool_calls:
        log("tool_call", f"tool:{call.name}", call.summary, content=call.args)
        log("tool_result", f"tool:{call.name}", call.result_summary,
            content=call.result, importance=2)

    remaind.update_state(base, lambda st: {**st, "estimated_context_tokens": total_tokens})

    cs = remaind.compaction_status(base)
    if cs.compaction_urgent:
        try:
            remaind.compact(base, compactor=compactor)
        except remaind.CompactionRejected:
            pass                                  # prior handover kept — safe

    if cs.band == "emergency":
        packet = remaind.resume(base).packet
        reset_model_context(packet.content)       # your code injects it

Choosing a compactor

compact(base, compactor=...) takes any object satisfying the Compactor protocol. Three ship with Remaind:

CompactorWhat it doesNeeds
default_compactRule-based bookkeeping — advances the anchor, rewrites a notes section. Does not summarize; the handover grows.nothing
LLMCompactorCalls the Anthropic API for a genuinely compressed handover.remaind[llm] + ANTHROPIC_API_KEY
CallableCompactorProvider-agnostic — inject any (system, user) → str | dict callable: a local model, a transformers pipeline, raw HTTP.you supply the transport

CallableCompactor is the seam for local models — Ollama, LM Studio, vLLM, llama.cpp, or a transformers pipeline. The callable receives the system + user prompts and returns a parsed payload dict or raw text (Remaind extracts the JSON — it handles fenced blocks and JSON embedded in prose). The validator is the same for every compactor.

import requests, remaind

def ollama(system: str, user: str) -> str:
    r = requests.post("http://localhost:11434/api/chat", json={
        "model": "llama3.1",
        "messages": [
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
        "format": "json",     # ask for JSON; CallableCompactor extracts it
        "stream": False,
    })
    return r.json()["message"]["content"]

compactor = remaind.CallableCompactor.from_context(base, ollama)
remaind.compact(base, compactor=compactor)

From the CLI: remaind compact --compactor mymodule:my_caller dynamically imports the callable.

CLI for shell-out harnesses

Harnesses that shell out rather than import can drive the lifecycle ops directly: remaind init, validate, status --json, compact, resume, rollback --to. Event logging, however, is library-only — there is no remaind log command, so the per-event append in step 2 must go through the Python API. See the Docs for the full CLI reference.

What Remaind does not do for you

  • It does not call your model or run your loop — you drive everything.
  • It does not see your real context window — it tracks the estimate you feed it.
  • It does not decide event importance — you tag each event.
  • It does not inject the resume packet — resume assembles it; placing it is your harness's job.
  • default_compact does not summarize — wire LLMCompactor or CallableCompactor for real compression.
  • It is single-writer — do not point two processes at one .context/.

Exception reference

ExceptionRaised byMeaning / what to do
InitErrorinit.context/ already exists — use force=True or a clean base.
EventValidationErrorEventWriter.appendthe event failed its schema — nothing was appended; inspect exc.event.
StateValidationErrorupdate_statethe mutator returned a schema-invalid state — state.json untouched.
HandoverValidationErrorwrite_handoverthe handover is missing a required section — see exc.missing.
CompactionRejectedcompactthe validator rejected the candidate — prior handover stands; inspect exc.validation.
CompactorConfigErrorLLMCompactor / CallableCompactormissing anthropic, missing key, or a non-callable was injected.
CompactorResponseErrorLLMCompactor / CallableCompactorAPI failure, truncated output, or an unparseable response — usually transient.
RollbackErrorrollbackunparseable target, or no snapshot covers it.