# Integrating Remaind into your agent

This guide shows how to wire Remaind into an agent harness. It uses only the
public API — `import remaind` — and every code block runs against the real
surface.

If you only want the CLI, see the [README](../README.md). This guide is for
operators integrating Remaind **programmatically** into a long-running agent.

---

## 1. Mental model

Remaind is a **substrate, not a framework.** It does not run your agent, call
your model, or manage your context window. It is the durable memory your
agent loop reads from and writes to. You own the loop; Remaind owns the
`.context/` directory.

```
   init ──▶ log events ──▶ track the budget ──▶ compact ──▶ resume into a
              ▲                                              fresh context
              │                                                  │
              └──────────────────────────────────────────────────┘
```

Three sources of truth live in `.context/`, with a strict authority order
(lower wins when they disagree):

1. Latest explicit user instruction
2. `logs/events.jsonl` — the append-only raw timeline
3. `active/state.json` — derived working state
4. `active/handover.md` — the compact continuity document
5. Derived memories

A stale summary can never override a newer user instruction. The full
contract is in `.context/CONTRACT.md` after you run `init`.

---

## 2. Install

```sh
pip install remaind
```

Two runtime dependencies, no optional extras. Remaind is **100% local** —
model-backed compaction talks to a local model runtime over HTTP using only
the Python standard library. No cloud API, no API key. Requires Python 3.11+.

---

## 3. The public API

`import remaind` is the entire stable surface. Everything in
`remaind.__all__` is stable; the underscore-prefixed modules
(`remaind._events`, …) are internal and may change — do not import from them.

```python
import remaind

remaind.__all__   # the full public surface
remaind.__version__
```

---

## 4. Initialize — once per project

```python
import remaind

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

Every event you log needs a `session_id` and a `task_id`. They are generated
by `init` and live in the state — read them once and hold them for the run:

```python
state = remaind.status(base)
session_id = state["session_id"]
task_id = state["task_id"]
```

---

## 5. Log events as the agent works

This is the continuous part of the loop. Open one `EventWriter` for the run
and append an event for every meaningful thing that happens.

```python
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** (`type=`): `user_message`, `assistant_message`, `tool_call`,
`tool_result`, `command_output`, `file_change`, `decision`, `error`,
`compaction`, `validation`, `resume`, `rollback`, `init`.

**Importance** drives what compaction must preserve — tag it honestly:

| importance | meaning | examples |
|---|---|---|
| `0` | trace / debug | verbose logs you will rarely need again |
| `1` | normal useful event | routine tool calls, assistant turns |
| `2` | decision, correction, constraint, file change, blocker, meaningful result | a design decision, a test passing, a file written, a blocker hit |
| `3` | explicit remember instruction, active next step, active blocker, critical user instruction | "always use X", the current next step, a hard blocker |

Compaction **must** keep every `importance >= 2` event in the summary chain;
`importance == 3` items must be explicitly represented in state or handover.
Get this wrong and either the handover bloats (everything tagged 3) or
load-bearing context is lost (everything tagged 1).

Two things happen automatically on every `append`:

- **Redaction.** Secrets (API keys, JWTs, AWS credentials, bearer tokens, …)
  are replaced with deterministic hashes *before* the event touches disk.
  You never have to scrub `content` yourself.
- **Large-output routing.** `content` over the byte threshold (or any
  `bytes` payload) is written to `.context/artifacts/` and the event stores
  head/tail excerpts + a SHA-256 + the artifact path — the raw log stays
  small.

A typical turn:

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

log("assistant_message", "assistant", "Proposed the refactor plan", content=reply)
log("tool_call", "tool:bash", "ran the test suite", content="pytest -q")
log("tool_result", "tool:bash", "all tests pass", content=test_output, importance=2)
log("decision", "assistant", "chose token format v2", content=rationale, importance=2)
```

You can also keep the derived files current as work progresses:

```python
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
})
```

`update_state` validates the result against the state schema, recomputes the
threshold band, and writes atomically with a history snapshot. A
schema-invalid mutation raises `StateValidationError` and leaves
`state.json` untouched.

---

## 6. Track the context budget

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

After each model turn, update the estimate and check the band:

```python
# `estimate_tokens` is the chars/4 approximation Remaind uses internally;
# substitute your model's real token count if you have it.
tokens = remaind.estimate_tokens(whole_context_so_far)
remaind.update_state(base, lambda st: {**st, "estimated_context_tokens": tokens})

cs = remaind.compaction_status(base)
print(cs.band)                 # "normal" | "warning" | "hard" | "emergency"
print(cs.recommendation)       # human-readable next action
if cs.compaction_needed:
    ...                        # see the next section
if cs.compaction_urgent:
    ...                        # band is "hard" or "emergency" — compact now
```

The default bands are 40k / 60k / 70k / 80k (`required` / `warning` / `hard`
/ `emergency`), configurable in `.context/schemas/thresholds.yaml`.

---

## 7. Compact when the band climbs

Compaction reduces the active context while preserving everything
load-bearing. **A compactor only ever 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, the
validator rejects it and the prior handover is kept untouched.

```python
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 was lost — the prior handover
    # stands. A `validation` event was appended recording why.
    print("rejected:", exc.validation["missing_items"], exc.validation["unsupported_claims"])
except remaind.CompactionError as exc:
    print("compaction could not run:", exc)
```

On accept, `compact` appends a `compaction` event and a `validation` event,
writes the new handover atomically (snapshotting the old one), and advances
the compaction anchor.

### Compaction uses your local model — automatically

`remaind.compact(base)` takes no model arguments. It is invisible by design:

- If a local **[Ollama](https://ollama.com)** runtime is running, Remaind
  detects it and compacts with your local model. Nothing to configure, no
  API key, nothing leaves your machine.
- If no local runtime is reachable, it falls back to `default_compact` —
  deterministic rule-based bookkeeping that never fails. (It advances the
  anchor and rewrites a `## Compaction Notes` section but does **not**
  actually summarize — the handover grows. Run a local model for real
  compression.)

Optional environment overrides — the zero-config path needs none:

| Variable | Effect |
|---|---|
| `OLLAMA_HOST` | Ollama's base URL (default `http://localhost:11434`). |
| `REMAIND_OLLAMA_MODEL` | Which installed model to use (default: the first Ollama reports). |
| `REMAIND_DISABLE_LOCAL_MODEL` | Set to force the rule-based compactor. |

Whatever the model returns is still a *candidate* — it flows through the
same structured validator as the rule-based compactor, so a weak local
model cannot corrupt context. The worst case is a rejected candidate and
the prior handover kept. An unreachable runtime or an unusable response
surfaces as `CompactorResponseError`.

Advanced: `compact(base, compactor=...)` still accepts any object satisfying
the `Compactor` protocol if you need a custom transport — see
`select_sources`, `CompactionSources`, and `CompactionCandidate`.

---

## 8. 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:

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

`resume` writes `.context/active/resume_packet.md`, appends a `resume`
event, and returns a `ResumeResult`. The packet bundles the handover,
selected state fields, FTS-retrieved memories, and recent high-importance
raw-log excerpts — token-capped.

**Injecting the packet is your job.** Remaind assembles it; your harness
decides where it goes — typically prepended to the new context's system
prompt or sent as the first user message:

```python
new_session = start_model_session(system_prompt=BASE_PROMPT + "\n\n" + fresh_context)
```

Before continuing, check the **resume gate**:

```python
gate = result.packet.gate
if gate.should_interrupt:
    # raw log / state / handover conflict, a missing next_step, an
    # unrepresented user instruction, or a risky next tool — surface to
    # the user instead of resuming silently.
    for concern in gate.concerns:
        print("RESUME GATE:", concern)
    if gate.urgent:
        ...   # low confidence AND the next tool mutates / spends money / has side effects
```

---

## 9. Rollback

Every state and handover write snapshots the prior version. If a compaction
or a series of edits went wrong, restore the derived files as of a
timestamp — `events.jsonl` is never touched:

```python
try:
    result = remaind.rollback(base, target="2026-05-14T03:54:33Z")
    print("restored:", result.restored, "skipped:", result.skipped)
except remaind.RollbackError as exc:
    print("rollback failed:", exc)
```

`target` accepts ISO 8601 or the filename-safe form (`20260514T035433Z`).
Rollback is itself reversible — it snapshots the pre-rollback files first.

---

## 10. A complete minimal agent loop

```python
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,
    ))

# No compactor to wire — remaind.compact() uses your local model if one is
# running, and the rule-based compactor otherwise.

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)

    # Keep the token estimate current.
    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)   # uses your local model if one is running
        except remaind.CompactionRejected:
            pass            # prior handover kept — safe to continue

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

---

## 11. What Remaind does NOT do for you

Be honest with yourself about the boundaries:

- **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 into `estimated_context_tokens`.
- **It does not decide event importance.** You tag every event; compaction
  fidelity depends on you tagging honestly.
- **It does not inject the resume packet.** `resume` assembles it; placing
  it into a fresh model context is your harness's job.
- **The rule-based fallback does not summarize.** When no local model is
  running, compaction is bookkeeping — the handover grows. Run a local
  model (Ollama) for real compression.
- **It is single-writer.** Do not point two processes at one `.context/`.
- **It does no remote sync, no hosted UI, no multi-agent coordination** —
  these are explicit v1 non-goals.

---

## 12. Exception reference

| Exception | Raised by | When | What to do |
|---|---|---|---|
| `InitError` | `init` | `.context/` already exists (without `force=True`), or a bad target | Use `init(base, force=True)` to back up + reinit, or pick a clean `base` |
| `EventValidationError` | `EventWriter.append` | the constructed event fails the event schema | A bug in the event you built — inspect `exc.event`; nothing was appended |
| `StateValidationError` | `update_state` | the mutator returned a schema-invalid state | Fix the mutator; `state.json` was left untouched |
| `HandoverValidationError` | `write_handover` | the new handover is missing a required H2 section | Add the missing section(s) in `exc.missing` |
| `CompactionRejected` | `compact` | the structured validator rejected the candidate | Nothing lost — prior handover stands. Inspect `exc.validation`; retry or compact a smaller window |
| `CompactorResponseError` | `compact` (local-model path) | the local model was unreachable or returned an unusable response | Transient — retry; check the Ollama runtime is up |
| `CompactionError` | `compact` | compaction could not run at all | Inspect the message; often an upstream problem (unreadable `.context/`) |
| `RollbackError` | `rollback` | unparseable `target`, or no snapshot covers it | Check the timestamp format and that history exists for that time |
| `StatusError` | `status` | `state.json` is missing or unreadable | Run `remaind validate` to diagnose the `.context/` |
