Metadata-Version: 2.4
Name: opentine-loop-engineering
Version: 0.1.0
Summary: Loop engineering toolkit with opentine-native branch/fork/replay provenance
Project-URL: Homepage, https://github.com/0xcircuitbreaker/loop-engineering-opentine
Project-URL: Source, https://github.com/0xcircuitbreaker/loop-engineering-opentine
Project-URL: Issues, https://github.com/0xcircuitbreaker/loop-engineering-opentine/issues
Project-URL: Changelog, https://github.com/0xcircuitbreaker/loop-engineering-opentine/blob/main/CHANGELOG.md
Author: Loop Engineering Community
License-Expression: MIT
License-File: LICENSE
Keywords: agent loops,ai engineering,fork,mcp,opentine,provenance,replay
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: opentine<0.5,>=0.2.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.9; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Description-Content-Type: text/markdown

# opentine-loop-engineering

A public, reusable setup for **Loop Engineering** backed by [`opentine`](https://github.com/0xcircuitbreaker/opentine): every loop step is a recorded node in a content-addressed `.tine` graph, with branch/fork/replay semantics for complex iterative systems.

## What this repo gives you

- `LoopEngine` for stateful, branchable, iterative loops.
- `LoopRecorder` wrapper around opentine `Run` for reliable provenance capture.
- Built-in deterministic strategies (`numeric_refinement`, `text_rewrite`) for local testing.
- CLI entrypoint for running loops and verifying artifacts.
- A testing scaffold to keep your loop contracts honest.
- Artifact-first workflow: every branch ends with a `.tine` file.

## Core design

```text
goal/state -> LoopEngine step_fn -> LoopStepResult
             -> recorder.model(step)
             -> branch fan-out -> forked branch runs via opentine
             -> completed runs -> tine artifacts
```

Each loop iteration gets saved as an opentine step node, so:

- You can **fork from any step** and re-run alternatives.
- You can **compare branches** with `Run.diff(...)`.
- You can **verify integrity** before replay or audit.
- You can build Nth-degree recursion without losing history.

## Quick start

```bash
pip install opentine-loop-engineering
pip install "opentine-loop-engineering[mcp]"   # adds the MCP server

# Run numeric refinement loop
loopforge run-numeric 42 --start 0 --max-steps 25

# Run text rewrite loop
loopforge run-text "clear and concise status update" --start "start draft" --good-word clear

# Write artifacts somewhere other than ~/.local/share/loopforge
loopforge run-numeric 42 --runs-dir ./runs

# Verify run (prints the failure reason and exits non-zero on tamper)
loopforge verify ./<run-id>.tine

# Compare two branches
loopforge compare left.tine right.tine
```

All `run-*` commands accept `--runs-dir` to choose where `.tine` artifacts are
written (default: `~/.local/share/loopforge`).

From a source checkout instead:

```bash
git clone https://github.com/0xcircuitbreaker/loop-engineering-opentine
cd loop-engineering-opentine
pip install -e ".[dev]"
pytest -q
```

## Repository structure

- `src/loopforge/engine.py` — loop orchestration, branching, policy gates.
- `src/loopforge/recorder.py` — opentine integration; the single `add_step` choke point.
- `src/loopforge/safety.py` — makes caller data recordable without raising.
- `src/loopforge/state.py` — state snapshots, digests and deltas.
- `src/loopforge/repo_backend.py` — v3 `.tine/` repositories (opentine 0.3.0+).
- `src/loopforge/usage.py` — provider token counts → opentine's exclusive dimensions.
- `src/loopforge/pricing.py` — signed rate-card pricing for a model step.
- `src/loopforge/policy.py` — step/cost/duration/score budgets.
- `src/loopforge/models.py` — model adapters and the JSON step builder.
- `src/loopforge/strategies/` — deterministic strategy examples.
- `src/loopforge/cli.py` — executable interface.
- `src/loopforge/mcp_server.py` — MCP tools over artifacts and repositories.
- `tests/` — unit tests; `.github/workflows/ci.yml` — CI across the supported opentine range.

## Example: branchable numeric loop

```python
from loopforge import LoopEngine
from loopforge.strategies import numeric_refinement

engine = LoopEngine(
    step_fn=numeric_refinement(target=42),
    max_steps=30,
    max_branches=4,
    branch_width=2,
)

result = engine.run(
    goal="Find x close to 42",
    initial_state={"x": 0.0},
    context="local optimization smoke test",
)

print(result.best.branch_id)
print(result.best.state)
print(result.best.score)

# result.best is the live branch object and keeps mutating during the run;
# result.best_snapshot is an immutable capture taken the moment the branch
# became best (branch_id, deep-copied state, score, step_id, run_id).
snap = result.best_snapshot
print(snap.branch_id, snap.score, snap.state, snap.step_id, snap.run_id)
```

Every active branch and each final candidate emits a `.tine` file under `~/.local/share/loopforge`.

## Why this helps with loop engineering

- **Complex loops**: branch fan-out and continuation at each iteration.
- **Controlled divergence**: cap active branches and branch width.
- **Auditability**: every step has parent links, timestamped costs, and integrity checksum.
- **Restartability**: fork and resume from any step boundary.

## API highlights

- `LoopEngine`:
  - `run(goal: str, initial_state: dict[str, Any], context: str = "") -> LoopExecutionResult`
- `LoopExecutionResult`:
  - `best` (live branch), `best_snapshot` (immutable `BestSnapshot` capture),
    `all_branches`, `best_step_result`, `artifacts`
- `LoopRecorder`:
  - `record_model`, `record_done`, `record_error`, `fork`, `from_run`, `save`
  - `sanitized_values` / `dropped_steps` counters; `strict=True` to re-raise
- `LoopStepResult`:
  - `observation`, `next_states`, `score`, `stop`, `metadata`
- Repositories (opentine 0.3.0+):
  - `open_repo`, `save_to_repo`, `load_from_repo`, `evaluate`, `candidates`, `promote`
- Cost:
  - `normalize_usage` (provider counts → exclusive dimensions), `price_call`,
    `billing_fields`

## Contributing

Run:

```bash
pytest -q
ruff check src tests
```


## What gets recorded, and how it is changed

loopforge records values it does not own — your loop state, step metadata, and
model outputs. Three things happen to them on the way to disk, and all three
are lossy in ways worth knowing before you rely on an artifact.

### 1. Recording never raises into your loop

Every payload is sanitized before opentine sees it, because canonical JSON
cannot represent everything a Python loop holds:

| In your state | In the artifact |
| --- | --- |
| `float("inf")` / `-inf` / `nan` | the strings `"Infinity"` / `"-Infinity"` / `"NaN"` |
| a reference cycle | `"<loopforge: circular reference>"` |
| nesting deeper than 64 | `"<loopforge: max depth exceeded>"` |
| a non-string dict key (`(0, 0)`, `b"k"`) | its text form; collisions get a `#2` suffix |
| any other object | a `repr` with the memory address stripped, so it is stable across copies |

`recorder.sanitized_values` counts every such substitution and
`recorder.dropped_steps` counts steps that could not be recorded at all.
Construct with `LoopRecorder(..., strict=True)` (or `LoopEngine(...,
strict_recording=True)`) to re-raise instead — worth doing in your own tests.

This matters most on the model path: `build_json_model_step` scores an
unparseable response `-inf`, which opentine 0.3.x refuses to write and 0.2.x
wrote as a bare `-Infinity` token — invalid JSON that **no later reader can
open**. Sanitizing is what keeps a run written under either version readable
by the other.

State nested thousands of levels deep is the one case that still refuses, with
`LoopStateError`: branches have to be isolated by copying, and a shallow copy
would let them silently corrupt each other.

### 2. opentine redacts by key name — and the rule differs by version

When a run is saved, opentine replaces recorded values whose **key** matches a
credential-name rule with `[REDACTED]`. Three properties matter more than the
list itself:

- It matches on the **key name, never the value**. A credential stored under a
  name it does not recognize is written in plaintext, and so is one embedded in
  a `prompt` or `observation` string.
- It is **irreversible and silent**. The integrity digest is computed over the
  already-redacted body, so `loopforge verify` reports `ok` and nothing in the
  artifact records that a value was replaced.
- Since state is recorded on every step, a matching key is redacted **on every
  step of every branch**, not once.

The rule is not the same across the supported range. opentine 0.2.0 matches a
**substring**; 0.3.0 and 0.4.0 match a **normalized name or suffix** (their
rules are identical — measured, not assumed). So the same loop state redacts
differently depending on which opentine is installed:

| state key | 0.2.0 | 0.3.0 / 0.4.0 | |
| --- | --- | --- | --- |
| `secret`, `api_key`, `password`, `token`, `authorization`, `credential`, `private_key`, `client_secret`, `access_token`, `refresh_token`, `session_token`, `auth_token`, `bearer_token`, `apiKey`, `user_password`, `my_secret`, `secrets`, `credentials` | redacted | redacted | agree |
| `passwd`, `passphrase`, `cookie` | **plaintext** | redacted | 0.2.0 misses real credential names |
| `session_key`, `public_key` | redacted | **plaintext** | newer misses a plausible credential name |
| `tokens`, `token_count`, `tokenizer`, `counter_token_total`, `secret_note`, `keyword`, `monkey` | redacted | **plaintext** | 0.2.0 destroys innocent data |
| a value containing `"authorization: ..."` mid-string | plaintext | rewritten | |

The third row is not a joke: on opentine 0.2.0 a state key named **`monkey`**
is redacted because it contains `key`. So is `keyword`, and so is a token
counter named `token_count`.

**What to do about it**

- **Do not rely on it to protect secrets.** Keep credentials out of loop state
  and out of prompts; pass them through closures or environment lookups that
  the recorder never sees.
- **Avoid these names for ordinary data.** If your loop legitimately works with
  a `secret` (a puzzle), a `token` (a word), or a `key` (a map lookup), rename
  the field — otherwise the value is gone from every artifact.
- **Check what actually landed**, rather than assuming:

  ```bash
  loopforge verify ./run.tine          # integrity only; pass --hmac-key for tamper evidence
  grep -c '\[REDACTED\]' ./run.tine    # this is what tells you
  ```

- **Pin the opentine version** you validated against if the difference matters
  to you; the same code produces different artifacts across the supported range.

A loopforge-side redaction policy that is deterministic across versions, applied
before the digest is computed, is designed but not yet implemented — see
[`docs/redaction-policy.md`](docs/redaction-policy.md).

### 3. State snapshots are bounded

A state whose canonical form exceeds 256 KiB is stored as a marker plus its
top-level key names, flagged `state_truncated`. The `state_digest` is still
taken over the *whole* state, so two states remain distinguishable even when
neither was stored in full.

Note that `state_digest` is computed **before** redaction, so for a redacted
key the artifact still carries a 64-bit commitment to the original value.

## Reading a run artifact

Every step that carries loop state records it under `outputs`:

| key | meaning |
| --- | --- |
| `state_after` | the state itself (see the 256 KiB bound above) |
| `state_digest` | short digest of the **whole** state, stable across processes |
| `state_truncated` | present and `true` only when `state_after` is partial |
| `candidates` | on a `loop_step`: each proposed next state's digest and delta |

Which step carries which state follows from how loopforge branches. `step_fn`
does not mutate `branch.state` — it evaluates the current state and *proposes*
`next_states`, and the branch adopts one afterwards. So a `loop_step` carries
the state that iteration **evaluated**, while `branch_continue` and
`branch_from_parent` carry the state the branch **adopted**.

```python
from opentine import Run

run = Run.load("path/to/run.tine")
for step_id in run.graph.order:
    step = run.graph.steps[step_id]
    state = (step.outputs or {}).get("state_after")
    if state is not None:
        print(step.kind.value, state)
```

Because forks get their own run and their own `.tine`, `candidates` is what
lets one artifact answer *how did the branches differ* without loading every
sibling artifact:

```python
for candidate in (step.outputs or {}).get("candidates", []):
    print(candidate["index"], candidate["delta"]["changed"])
```

## opentine compatibility

Works on opentine 0.2.0, 0.3.0 and 0.4.0; the dependency is pinned as
`opentine>=0.2.0,<0.5`. Step metrics (cost/duration) are clamped to finite,
non-negative values, and every recorded payload is sanitized (see above), so
artifacts stay valid — and mutually readable — across all three.

Newer-only features degrade cleanly and are selected by capability, never by
version string:

| Feature | Needs | Without it |
|---|---|---|
| v3 `.tine/` repositories | 0.3.0 | `repo_available()` is `False`; the `repo-*` commands exit with a message |
| Priced billing records | 0.3.0 | the caller's own `cost` is recorded, unpriced |
| Per-act fork identity | 0.4.0 | loopforge mints its own unique branch ids instead |

## v3 repositories (opentine 0.3.0+)

A loop writes one artifact per branch. A repository stores them as
content-addressed objects that share their common ancestry instead of
duplicating it, and adds compare-and-swap refs, evaluation attestations and
release-gate promotion:

```python
from loopforge import open_repo, save_to_repo, evaluate, candidates, promote

repo = open_repo("~/loops", create=True)
result = engine.run(goal="converge", initial_state={"x": 0.0})

stored = save_to_repo(result.best.recorder, repo, ref="experiments/best")
evaluate(repo, stored["run_id"], {"gate": result.best.score}, signer="ci")
best = candidates(repo, min_score=0.9)          # searchable by score
promote(repo, stored["run_id"], "prod", signer="ci")
```

loopforge writes only `experiments/*`, `heads/loopforge/*` and `tags/*` — a
mainline head or a release gate is an explicit operator action, never a side
effect of recording. Tags, step counts and duplicate parent ids are checked
*before* anything is written, because opentine validates them only after
every event object is already stored.

## Branch identity

Two branches taken from the same step must not collide. Before opentine
0.4.0 a fork id was a pure function of (parent, fork point), so the second
branch's save silently destroyed the first — loopforge therefore minted its
own unique id. opentine 0.4.0 derives the id from the fork *act* (lineage,
retained slice, branch, declared intent, random nonce) and records the basis,
so a fork can prove its own id. loopforge now defers to that where available
and keeps its own scheme where it is not.

## Cost, tokens and signing

Model steps carry `model_info`, so `cost_breakdown().by_model` attributes
spend instead of pooling it in one anonymous bucket. Provider token counts are
normalized onto opentine's **exclusive** dimensions — OpenAI's
`prompt_tokens` already contains `cached_tokens` and its `completion_tokens`
already contains `reasoning_tokens`, so mapping them straight across counts
the cheap cached tokens twice and overstates spend. With a `provider`, a step
is priced against opentine's signed rate-card catalog and carries the catalog
id and an explicit complete/partial status; an unpriceable model keeps the
caller's own figure rather than being zeroed.

```python
recorder.save(path, sign_key=key, signer="ci")   # tamper evidence
```

A signature covers the run body plus an allowlist of metadata keys whose
membership depends on the opentine version (it grew in 0.4.0). Run tags are
**outside** it in every version, and opentine refuses to sign a run that has
not finished. `loopforge verify` reports the installed allowlist and
`tags_covered_by_signature` rather than asserting a count.

Because that allowlist grew, **sign and verify within one opentine
generation**. Every loopforge branch is a fork, and 0.4.0 both writes
`metadata.fork` and adds `fork` to the allowlist — so a branch artifact signed
under 0.4.0 does not verify under 0.2.0/0.3.0, which do not cover that key.
Integrity verification is unaffected in either direction.

## Notes

- Portable `.tine` artifacts stay format v2 on every supported opentine, so a run
  written under one version opens under the others.
- Artifacts are written under `~/.local/share/loopforge` unless `--runs-dir` or
  `LoopEngine(runs_dir=...)` says otherwise.
- Never put a `config.json` in the runs directory: from opentine 0.3.0,
  `Run.save`/`Run.load` treat a directory containing one as a v3 repository
  rather than a path to write a `.tine` into.



## Policy and model-backed execution

`max_total_cost` is a run-wide spend ceiling: the engine sums the cost of every
executed step exactly once (across all branches, with no double-counting on
forks) and gates the run when the ceiling is crossed. Gated branches record a
`policy_gate` think step, a done step carrying the reason, and a run tag such
as `gate:max_total_cost`.

```python
from loopforge import LoopEngine, LoopPolicy
from loopforge.models import StaticModelAdapter, build_json_model_step
from loopforge.engine import LoopStepContext

policy = LoopPolicy(
    max_steps=12,
    max_total_cost=0.75,
    min_score=0.95,
    max_duration_seconds=30.0,
)

adapter = StaticModelAdapter(
    text='{"observation":"ok","score":0.97,"stop":true,"next_states":[]}',
    cost=0.01,
)

# Parse JSON responses shaped as {observation, score, stop, next_states, metadata}
def prompt_fn(ctx: LoopStepContext) -> str:
    return f"[{ctx.branch_id}] improve text: {ctx.current_state.get('text')}"

step_fn = build_json_model_step(adapter=adapter, prompt_fn=prompt_fn)
engine = LoopEngine(step_fn=step_fn, policy=policy, max_steps=5)
result = engine.run(goal="policy-aware loop", initial_state={"text": "start"})
print(result.best.score, result.best.recorder.run.run_id)
```

## MCP integration

A built-in MCP server exposes loop artifacts as tools for editor/agent automation:

- `list_run_artifacts`
- `show_run_tool`
- `diff_run_artifacts`
- `fork_run_artifact`
- `show_loop_trajectory` — iterations, score trajectory, where the best score
  was first reached, and how the branch terminated
- `loop_diff_runs` — compare two branches at the LOOP layer: trajectories,
  convergence, and the first iteration at which they diverge

Every ref is resolved *inside* the runs directory (no traversal, no symlink
escape), artifacts are size-checked before loading, and responses are capped.
When a v3 repository is available, opentine's own repository tools are
composed onto the same server — `repo_status` and `list_repo_candidates` are
added, and promotion is never exposed.

Install the extra and start it with:

```bash
pip install "opentine-loop-engineering[mcp]"
loopforge-mcp --runs-dir ~/.local/share/loopforge
# or via the main CLI
loopforge mcp-server --runs-dir ~/.local/share/loopforge
```

