-
Notifications
You must be signed in to change notification settings - Fork 0
M4 AP-4: BYOM example + docs (guide, API reference, offline example, CI smoke) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
fdd6b3a
M4 AP-4: plan the BYOM example + docs slice (example, guide, API ref,…
STiFLeR7 208000a
M4 AP-4: export FileCheckpointStore/FileEffectLedger (public referenc…
STiFLeR7 94edaf2
M4 AP-4: BYOM example (primitives + Agent + exactly-once effect) + CI…
STiFLeR7 2c3dd95
M4 AP-4: BYOM guide (recovery in your own agent) + README pointer
STiFLeR7 091b4e4
M4 AP-4: public API reference (cairn.__all__ surface)
STiFLeR7 3ab5fe4
M4 AP-4: minimal CI (pytest on 3.10/3.12; runs the BYOM smoke test)
STiFLeR7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master, "milestone-*", "phase-*"] | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| python-version: ["3.10", "3.12"] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| - name: Install (dev extras) | ||
| run: pip install -e ".[dev]" | ||
| - name: Run test suite (includes the BYOM example smoke test) | ||
| run: python -m pytest -q | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # Public API reference | ||
|
|
||
| Everything below is re-exported from the top-level `cairn` package (`from cairn import ...`). | ||
| Cairn is **0.x**; this surface is stabilizing but not frozen until v1.0 (held — see the | ||
| [claims registry](../research/claims-registry.md)). | ||
|
|
||
| ## Types | ||
|
|
||
| | Name | What it is | | ||
| |---|---| | ||
| | `Action` | One proposed step: `Action(kind, code="", result="")`. `kind` is `"code"` or `"finish"`. | | ||
| | `StepRecord` | An executed step: `StepRecord(step, action, returncode=0, stdout="", stderr="")`. The unit of `history`. | | ||
| | `Checkpoint` | A durable checkpoint (the "cairn"). Transparent alias of the internal `ContinuationState`. | | ||
| | `Regrounded` | Result of `recover()`: `.history`, `.resolutions`, `.plan` (`plan is None` ⇒ no checkpoint). | | ||
| | `Resolution` | What recovery did to one effect: `.key`, `.tool_class`, `.action` (`"redo"`/`"skip"`/`"escalate"`), `.detail`. | | ||
| | `ResumePlan` | The reconciled resume plan (torn-step + danger window) produced during `recover()`. | | ||
| | `AgentRun` | Outcome of `Agent.run`/`.resume`: `.finished`, `.steps`, `.checkpoints`, `.history`, `.final_state`, `.resumed`, `.recovery_tax`, `.resolutions`. | | ||
|
|
||
| ## Contracts (Protocols you implement or accept) | ||
|
|
||
| | Name | Method surface | | ||
| |---|---| | ||
| | `World` | `snapshot() -> str`, `restore(snap_id) -> None`, `digest() -> dict`. (The `Agent` loop also needs `execute(code, cwd=None) -> ExecResult`.) | | ||
| | `CheckpointStore` | `checkpoint(state, snap_id, effect_offset) -> str`, `load_latest() -> tuple | None`. | | ||
| | `EffectLedger` | `append_effect(intent, key, tool_class, step)`, `complete_effect(key)`, `current_offset() -> int`, `list_effects_since(offset)`. | | ||
|
|
||
| ## Effect safety | ||
|
|
||
| | Name | What it is | | ||
| |---|---| | ||
| | `EffectfulTool` | An injected irreversible effect: `EffectfulTool(name, tool_class, run=None, verify=None)`. | | ||
| | `EscalationRequired` | Raised when a `never-retry` effect is found in the danger window (unless `escalate=False`). | | ||
|
|
||
| ## Reference implementations (bundled) | ||
|
|
||
| | Name | What it is | | ||
| |---|---| | ||
| | `Workspace` | A local-filesystem `World` (also executable). `Workspace(workspace_dir, snapshots_dir, sandbox=None, interpreter=None)`. | | ||
| | `FileCheckpointStore` | File-backed `CheckpointStore`. `FileCheckpointStore(ckpt_dir)`. | | ||
| | `FileEffectLedger` | Append-only file-backed `EffectLedger`. `FileEffectLedger(path, ledger_id="default")`. | | ||
|
|
||
| ## Primitives (bring your own loop) | ||
|
|
||
| **`checkpoint(goal, history, world, store, ledger, *, step, model_version="", harness_version="") -> Checkpoint`** | ||
| Distill `history` into a cairn, snapshot the world, and persist atomically. Returns the durable | ||
| `Checkpoint`. | ||
|
|
||
| **`recover(world, store, ledger, *, effect_tools=None, escalate=True) -> Regrounded`** | ||
| The full RGR protocol: load latest checkpoint → `world.restore` → re-observe → reconcile the torn | ||
| step → resolve the effect danger window (exactly-once) → re-ground a minimal history. No durable | ||
| checkpoint ⇒ empty history (start fresh). The goal is carried in the checkpoint, not passed here. | ||
|
|
||
| **`regrounded_history(plan_steps) -> list[StepRecord]`** | ||
| Low-level helper: reconstruct a minimal history from a checkpoint's *done* steps (faithful | ||
| summaries, ~80 chars each — re-grounding, not byte-exact replay). Used internally by `recover()`. | ||
|
|
||
| ## Opt-in loop (batteries-included) | ||
|
|
||
| **`Agent(model, world, *, store, ledger, effect_tools=None, max_steps=20, escalate=True, model_version="byom", harness_version="")`** | ||
| - `run(goal) -> AgentRun` — drive from scratch, checkpointing each executed step. | ||
| - `resume(goal) -> AgentRun` — re-ground via `recover()` and continue; falls back to `run` when | ||
| there is no checkpoint. | ||
|
|
||
| ## Version | ||
|
|
||
| `cairn.__version__` — the package version (currently `0.x`). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # Recovery in your own agent (BYOM) | ||
|
|
||
| Cairn is a **bring-your-own-model** recovery library. You keep your model, your keys, and your | ||
| tools; Cairn adds **crash-recovery** to your agent through *Re-grounding Recovery (RGR)* — on | ||
| resume it loads the last checkpoint, restores your world, re-observes reality, reconciles the | ||
| step that was in flight, resolves any half-finished side-effect **exactly once**, and re-grounds | ||
| a compact history so your model continues instead of starting over. | ||
|
|
||
| > **Status (honest scope).** Cairn is **0.x** and not published. In the deterministic reference | ||
| > harness, RGR beats cold restart and the effect WAL yields zero duplicate effects. The headline | ||
| > claim (RGR beats cold restart on a *live* model, "C1") is **not yet confirmed** — it awaits a | ||
| > powered live-LLM study. This library exists partly so you can reproduce the evidence on *your* | ||
| > model. See [`docs/research/claims-registry.md`](../research/claims-registry.md). | ||
|
|
||
| ## Install | ||
|
|
||
| Cairn isn't on PyPI (0.x). Use it from a checkout: | ||
|
|
||
| ```bash | ||
| git clone https://github.com/STiFLeR7/Cairn && cd Cairn | ||
| pip install -e ".[dev]" # or: PYTHONPATH=src python your_script.py | ||
| ``` | ||
|
|
||
| ## The two seams you bring | ||
|
|
||
| - **A `Model`** — any object with `propose(goal, history) -> Action`. Cairn ships | ||
| `ScriptableMockModel` for tests/examples; a real local adapter (Ollama) is shown below. | ||
| - **A `World`** — the observable, snapshottable thing your agent acts on. Cairn ships | ||
| `Workspace` (a local filesystem workspace). For the `Agent` loop the World must also be | ||
| *executable* (`execute(code) -> ExecResult`); `Workspace` is. | ||
|
|
||
| Recovery infrastructure (`FileCheckpointStore`, `FileEffectLedger`) is bundled — you don't | ||
| implement it. | ||
|
|
||
| ## Path A — keep your own loop (primitives) | ||
|
|
||
| Call `checkpoint(...)` after each step; call `recover(...)` after a crash and continue from the | ||
| history it returns: | ||
|
|
||
| ```python | ||
| from cairn import (Action, StepRecord, Workspace, | ||
| FileCheckpointStore, FileEffectLedger, checkpoint, recover) | ||
|
|
||
| world = Workspace("ws", "snaps") | ||
| store = FileCheckpointStore("ck") | ||
| ledger = FileEffectLedger("effects.jsonl", "run") | ||
|
|
||
| history = [] | ||
| for step, action in enumerate(your_actions): | ||
| world.execute(action.code) # apply with YOUR tools | ||
| history.append(StepRecord(step=step, action=action, returncode=0)) | ||
| checkpoint(goal, history, world, store, ledger, step=step) | ||
|
|
||
| # ... process dies mid-run ... | ||
|
|
||
| rg = recover(world, store, ledger) # load -> restore -> re-observe -> reconcile | ||
| history = list(rg.history) # re-grounded prior steps (summaries, not replay) | ||
| # continue your loop from `history` | ||
| ``` | ||
|
|
||
| `recover()` with **no checkpoint** returns an empty history (start fresh). The `goal` is *not* a | ||
| parameter to `recover` — it is carried inside the loaded checkpoint. | ||
|
|
||
| ## Path B — batteries-included (`Agent`) | ||
|
|
||
| Same mechanism, none of the loop: | ||
|
|
||
| ```python | ||
| from cairn import Agent, Workspace, FileCheckpointStore, FileEffectLedger | ||
|
|
||
| agent = Agent(your_model, Workspace("ws", "snaps"), | ||
| store=FileCheckpointStore("ck"), | ||
| ledger=FileEffectLedger("effects.jsonl", "run")) | ||
|
|
||
| run = agent.run(goal) # checkpoints every executed step | ||
| # ... crash ... | ||
| run = agent.resume(goal) # re-grounds via recover(), then finishes | ||
| print(run.resumed, run.recovery_tax, run.finished) | ||
| ``` | ||
|
|
||
| `recovery_tax` is the number of steps executed *after* resume — the cost of recovery vs. a cold | ||
| restart that would redo everything. `Agent` has no task oracle: it reports whether the model | ||
| finished and hands back the full history + last checkpoint so you apply your own success test. | ||
|
|
||
| ## Side-effects, exactly once | ||
|
|
||
| An irreversible effect (send an email, charge a card) must survive a crash *between* "it | ||
| happened" and "we recorded that it happened". Wrap it as an `EffectfulTool` and register its | ||
| intent in the ledger **before** acting; on resume, `recover(effect_tools=...)` resolves the | ||
| danger window: | ||
|
|
||
| ```python | ||
| from cairn import EffectfulTool | ||
|
|
||
| ledger.append_effect("send_report", "send-1", "check-before-retry", step=step) # INTENT first | ||
| do_the_send() # then the effect | ||
| # crash before we can mark it complete ... | ||
|
|
||
| tool = EffectfulTool("send_report", "check-before-retry", | ||
| run=do_the_send, | ||
| verify=lambda: report_already_sent()) # True iff it already happened | ||
| rg = recover(world, store, ledger, effect_tools={"send-1": tool}) | ||
| # verify() -> True => Resolution(action="skip"): the effect is NOT re-executed (no duplicate) | ||
| ``` | ||
|
|
||
| Tool classes: `safe-to-retry` (redo), `check-before-retry` (verify then redo-or-skip), | ||
| `never-retry` (escalate — a human decides). See | ||
| [`docs/design/effect-safety-protocol.md`](../design/effect-safety-protocol.md). | ||
|
|
||
| ## Run it | ||
|
|
||
| The full, runnable version of all three snippets is | ||
| [`examples/byom_recovery.py`](../../examples/byom_recovery.py): | ||
|
|
||
| ```bash | ||
| python examples/byom_recovery.py | ||
| ``` | ||
|
|
||
| It uses a deterministic mock model — no API key, no network — and is exercised in CI. | ||
|
|
||
| ## Use a real local model (Ollama) | ||
|
|
||
| `examples/byom_recovery.py` includes `_ollama_model()`, a stdlib-only adapter that satisfies the | ||
| `Model` seam by calling a local [Ollama](https://ollama.com) server. It is **opt-in** and never | ||
| runs in CI (no network there). To try it: | ||
|
|
||
| ```bash | ||
| ollama serve & # in another shell | ||
| ollama pull llama3.2 | ||
| CAIRN_OLLAMA=1 python examples/byom_recovery.py | ||
| ``` | ||
|
|
||
| Swapping `ScriptableMockModel` for `_ollama_model()` is the whole point of BYOM: the recovery | ||
| machinery is identical — only the model changed. Reproduce the recovery-vs-cold-restart | ||
| comparison on your own model and report what you find. | ||
|
|
||
| ## What's next | ||
|
|
||
| - Full names and signatures: [public API reference](public-api-reference.md). | ||
| - Why re-grounding (not replay): [`docs/concepts/recovery-fidelity.md`](../concepts/recovery-fidelity.md). |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this checkout the integration branch is
main(the recent merge commits are onmain, and there is no localmasterbranch), so this workflow will not run on post-merge or direct pushes to the default branch; it only runs formaster,milestone-*, andphase-*. Please addmainhere or remove the branch filter so CI actually protects the branch changes land on.Useful? React with 👍 / 👎.