Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,16 @@ jobs:
run: |
python -B -m loop doctor examples/coverage-repair
python -B -m loop inspect examples/coverage-repair

recipe-langgraph:
name: recipe (langgraph)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install recipe dependencies
run: python -m pip install --upgrade pip pyyaml pytest jsonschema langgraph
- name: LangGraph recipe end-to-end
run: python -B -m pytest -q -p no:cacheprovider scripts/test_langgraph_recipe.py
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,31 @@ All notable changes to `loop-engineer` are documented here.
`WORKFLOW.md` and `README.md` are reworded to describe the mechanism; the 0.3.4
history is left intact.

## Unreleased

**B1 — the writer API.** `loop.emit` lets a foreign runtime (LangGraph, a plain
script, any orchestrator) record an evidence-backed loop contract without
adopting the loop-engineer runtime. It is a writer, never a runtime: it renders
the contract artifacts and refuses a dishonest `Succeeded` at write time — the
same evidence cross-check `loop doctor` enforces, applied before the file exists.

### Added
- **`loop/emit.py` writer API** — `open_contract`, `append_iteration`,
`append_receipt`, and `terminate`, plus the `EmitError` raised when a write
would produce a dishonest or schema-invalid artifact. `terminate` refuses an
evidence-free `Succeeded` (also no-met-criterion or false-completion-flagged),
so the honesty gate runs at write time rather than only at validate time;
every artifact it writes passes `doctor` by construction.
- **LangGraph recipe** (`examples/langgraph-emit/`) — a runnable three-node
graph whose terminal node ships proof-of-done through `loop.emit`; the emitted
contract passes `loop doctor` independently of the graph that wrote it. Paired
with the 10-line integration guide `docs/integrations/langgraph.md`.
- **Recipe acceptance test** (`scripts/test_langgraph_recipe.py`) — runs the
example end-to-end and asserts the emitted contract passes `doctor` and ends
`Succeeded` with evidence. Env-guarded on `langgraph` (skips when absent), so
the package stays zero-dependency; a dedicated `recipe (langgraph)` CI job
installs LangGraph and runs it.

## 0.6.1 — 2026-07-04

**PyPI substrate.** `loop-engineer` becomes a self-contained wheel that runs from
Expand Down
30 changes: 30 additions & 0 deletions docs/integrations/langgraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# LangGraph — proof-of-done in 10 lines

`loop.emit` is a pure-stdlib writer: your graph keeps its own runtime, and the
terminal node records evidence-backed state the `loop` CLI can independently
validate. `pip install loop-engineer` (LangGraph itself stays your dependency).

```python
from loop import emit

emit.open_contract("run/") # once, before the graph runs

def conclude(state): # your graph's terminal node
emit.append_iteration("run/", iteration_id=1, outcome="task_passed",
task_id="T1", verify_cmd="pytest -q", verify_outcome="pass")
emit.terminate("run/", state="Succeeded",
criteria_met={"tests": True}, evidence=["reports/pytest.txt"])
return {}
```

`emit.terminate` **refuses an evidence-free `Succeeded`** (raises `EmitError`) —
the same cross-check `loop doctor` enforces, applied before the file exists.

Gate it in CI:

```yaml
- run: pip install loop-engineer
- run: loop doctor run/
```

Full runnable example: [`examples/langgraph-emit/`](../../examples/langgraph-emit/).
34 changes: 34 additions & 0 deletions examples/langgraph-emit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# LangGraph recipe — proof-of-done through `loop.emit`

A runnable [LangGraph](https://github.com/langchain-ai/langgraph) graph whose
**terminal node writes the loop contract** — evidence-backed state the `loop`
CLI can independently validate. LangGraph keeps its own runtime; `loop.emit` is
a pure-stdlib writer that refuses to record a dishonest result.

## What it shows

`graph_example.py` runs three plain-function nodes — `do_work` writes
`artifact.txt`, `verify` re-reads it from disk, and `conclude` records the
outcome:

- On a real pass, `conclude` calls `emit.terminate(..., state="Succeeded",
evidence=["artifact.txt"])`.
- A lying `Succeeded` — no evidence, or no met criterion — raises `EmitError`
**before anything hits disk**. That is the same cross-check `loop doctor`
enforces, applied at write time.

## Run it

```bash
pip install loop-engineer langgraph
python graph_example.py demo-run/
loop doctor demo-run/ # -> {"ok": true, ...}
```

`demo-run/.loop/terminal_state.json` ends `Succeeded` with `evidence`; `loop
doctor` validates it independently of the graph that wrote it.

## The 10-line integration

The general pattern (any graph, any terminal node) lives in
[`docs/integrations/langgraph.md`](../../docs/integrations/langgraph.md).
82 changes: 82 additions & 0 deletions examples/langgraph-emit/graph_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""A minimal LangGraph graph that ships proof-of-done through loop.emit.

The graph does real (tiny) work, verifies it from the filesystem, and the
terminal node records the outcome via emit.terminate(...) — which refuses an
evidence-free Succeeded. Run:

python graph_example.py <fresh-workspace-dir>
"""

from __future__ import annotations

import sys
from pathlib import Path
from typing import TypedDict

from langgraph.graph import END, START, StateGraph

from loop import emit


class State(TypedDict):
workspace: str
verified: bool


def do_work(state: State) -> dict:
out = Path(state["workspace"]) / "artifact.txt"
out.write_text("hello from langgraph\n", encoding="utf-8")
return {}


def verify(state: State) -> dict:
artifact = Path(state["workspace"]) / "artifact.txt"
ok = artifact.is_file() and "hello" in artifact.read_text(encoding="utf-8")
return {"verified": ok}


def conclude(state: State) -> dict:
ws = state["workspace"]
passed = state["verified"]
emit.append_iteration(
ws, iteration_id=1, outcome="task_passed" if passed else "task_failed",
task_id="T1", actions=["wrote artifact.txt", "re-read and checked content"],
verify_cmd="verify node (filesystem re-read)", verify_outcome="pass" if passed else "fail",
)
if passed:
emit.terminate(
ws, state="Succeeded", criteria_met={"1": True},
evidence=["artifact.txt"], reason="artifact written and independently re-read",
iteration_id=1,
)
else:
emit.terminate(
ws, state="FailedUnverifiable", criteria_met={"1": False},
evidence=[], reason="verification failed", iteration_id=1,
)
return {}


def main(workspace: str) -> int:
emit.open_contract(workspace)
graph = (
StateGraph(State)
.add_node(do_work)
.add_node(verify)
.add_node(conclude)
.add_edge(START, "do_work")
.add_edge("do_work", "verify")
.add_edge("verify", "conclude")
.add_edge("conclude", END)
.compile()
)
graph.invoke({"workspace": workspace, "verified": False})
print(f"contract emitted at {workspace}/.loop — run: python3 -m loop doctor {workspace}")
return 0


if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: python graph_example.py <fresh-workspace-dir>", file=sys.stderr)
raise SystemExit(2)
raise SystemExit(main(sys.argv[1]))
Loading
Loading