opentine-loop-engineering
A public, reusable setup for Loop Engineering backed by 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
LoopEnginefor stateful, branchable, iterative loops.LoopRecorderwrapper around opentineRunfor 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
.tinefile.
Core design
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
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:
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 singleadd_stepchoke 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
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(immutableBestSnapshotcapture),all_branches,best_step_result,artifacts
LoopRecorder:record_model,record_done,record_error,fork,from_run,savesanitized_values/dropped_stepscounters;strict=Trueto 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:
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
promptorobservationstring. - It is irreversible and silent. The integrity digest is computed over the
already-redacted body, so
loopforge verifyreportsokand 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), atoken(a word), or akey(a map lookup), rename the field — otherwise the value is gone from every artifact. -
Check what actually landed, rather than assuming:
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.
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.
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:
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:
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.
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
.tineartifacts stay format v2 on every supported opentine, so a run written under one version opens under the others. - Artifacts are written under
~/.local/share/loopforgeunless--runs-dirorLoopEngine(runs_dir=...)says otherwise. - Never put a
config.jsonin the runs directory: from opentine 0.3.0,Run.save/Run.loadtreat a directory containing one as a v3 repository rather than a path to write a.tineinto.
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.
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_artifactsshow_run_tooldiff_run_artifactsfork_run_artifactshow_loop_trajectory— iterations, score trajectory, where the best score was first reached, and how the branch terminatedloop_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:
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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file opentine_loop_engineering-0.1.0.tar.gz.
File metadata
- Download URL: opentine_loop_engineering-0.1.0.tar.gz
- Upload date:
- Size: 92.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30de3194cce7c0b2784691fd1fb0cd0ca68da0b61b84f236d79eb975a607f098
|
|
| MD5 |
4c8d8a0af5b62708f4c63e31f40cea2c
|
|
| BLAKE2b-256 |
6fc9c88013325d7a318c29af160fb7ccc92ddacbb722455f79186a2b080418e2
|
File details
Details for the file opentine_loop_engineering-0.1.0-py3-none-any.whl.
File metadata
- Download URL: opentine_loop_engineering-0.1.0-py3-none-any.whl
- Upload date:
- Size: 69.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
971994331fa86b00032befc61a7646c336d1e91460fae688905d6d714ad1c2e5
|
|
| MD5 |
0dab7d661b0a8348dababee49dd5ba37
|
|
| BLAKE2b-256 |
d4dbee9facaf9c328c9ec6e4cda91bf19606feea6232dde500ea1621c04c7203
|