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 compactorThe [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 toolA 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 itChoosing a compactor
compact(base, compactor=...) takes any object satisfying the Compactor protocol. Three ship with Remaind:
| Compactor | What it does | Needs |
|---|---|---|
| default_compact | Rule-based bookkeeping — advances the anchor, rewrites a notes section. Does not summarize; the handover grows. | nothing |
| LLMCompactor | Calls the Anthropic API for a genuinely compressed handover. | remaind[llm] + ANTHROPIC_API_KEY |
| CallableCompactor | Provider-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
| Exception | Raised by | Meaning / what to do |
|---|---|---|
| InitError | init | .context/ already exists — use force=True or a clean base. |
| EventValidationError | EventWriter.append | the event failed its schema — nothing was appended; inspect exc.event. |
| StateValidationError | update_state | the mutator returned a schema-invalid state — state.json untouched. |
| HandoverValidationError | write_handover | the handover is missing a required section — see exc.missing. |
| CompactionRejected | compact | the validator rejected the candidate — prior handover stands; inspect exc.validation. |
| CompactorConfigError | LLMCompactor / CallableCompactor | missing anthropic, missing key, or a non-callable was injected. |
| CompactorResponseError | LLMCompactor / CallableCompactor | API failure, truncated output, or an unparseable response — usually transient. |
| RollbackError | rollback | unparseable target, or no snapshot covers it. |