Skip to main content

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

  • 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

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 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

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:

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:

    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 .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.

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:

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

opentine_loop_engineering-0.1.0.tar.gz (92.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

opentine_loop_engineering-0.1.0-py3-none-any.whl (69.4 kB view details)

Uploaded Python 3

File details

Details for the file opentine_loop_engineering-0.1.0.tar.gz.

File metadata

File hashes

Hashes for opentine_loop_engineering-0.1.0.tar.gz
Algorithm Hash digest
SHA256 30de3194cce7c0b2784691fd1fb0cd0ca68da0b61b84f236d79eb975a607f098
MD5 4c8d8a0af5b62708f4c63e31f40cea2c
BLAKE2b-256 6fc9c88013325d7a318c29af160fb7ccc92ddacbb722455f79186a2b080418e2

See more details on using hashes here.

File details

Details for the file opentine_loop_engineering-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for opentine_loop_engineering-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 971994331fa86b00032befc61a7646c336d1e91460fae688905d6d714ad1c2e5
MD5 0dab7d661b0a8348dababee49dd5ba37
BLAKE2b-256 d4dbee9facaf9c328c9ec6e4cda91bf19606feea6232dde500ea1621c04c7203

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page