cairns 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. cairns-0.2.0/.envrc +1 -0
  2. cairns-0.2.0/.gitignore +6 -0
  3. cairns-0.2.0/.import_linter_cache/.gitignore +2 -0
  4. cairns-0.2.0/.import_linter_cache/6ff22d18cc797dcfbda9e438db40df94c9091ca4.data.json +1 -0
  5. cairns-0.2.0/.import_linter_cache/CACHEDIR.TAG +3 -0
  6. cairns-0.2.0/.import_linter_cache/cairn.meta.json +1 -0
  7. cairns-0.2.0/.import_linter_cache/cairns.meta.json +1 -0
  8. cairns-0.2.0/.import_linter_cache/ea33efcc55ca9faff44446ae7f31abced923d943.data.json +1 -0
  9. cairns-0.2.0/LICENSE +21 -0
  10. cairns-0.2.0/PKG-INFO +178 -0
  11. cairns-0.2.0/README.md +136 -0
  12. cairns-0.2.0/Taskfile.yaml +34 -0
  13. cairns-0.2.0/devenv.lock +103 -0
  14. cairns-0.2.0/devenv.nix +21 -0
  15. cairns-0.2.0/devenv.yaml +6 -0
  16. cairns-0.2.0/docs/design.md +453 -0
  17. cairns-0.2.0/docs/logo.jpg +0 -0
  18. cairns-0.2.0/docs/motivation.md +80 -0
  19. cairns-0.2.0/docs/patterns.md +529 -0
  20. cairns-0.2.0/docs/store-design.md +617 -0
  21. cairns-0.2.0/docs/todo/anyio.md +34 -0
  22. cairns-0.2.0/docs/todo/nominal-identity.md +174 -0
  23. cairns-0.2.0/examples/failure_resume.py +90 -0
  24. cairns-0.2.0/examples/hitl.py +65 -0
  25. cairns-0.2.0/examples/research_fake_llm.py +182 -0
  26. cairns-0.2.0/examples/research_haiku.py +143 -0
  27. cairns-0.2.0/examples/rlhf_mock.py +260 -0
  28. cairns-0.2.0/examples/scraper.py +110 -0
  29. cairns-0.2.0/pyproject.toml +100 -0
  30. cairns-0.2.0/src/cairns/__init__.py +50 -0
  31. cairns-0.2.0/src/cairns/cli/__init__.py +219 -0
  32. cairns-0.2.0/src/cairns/core/__init__.py +91 -0
  33. cairns-0.2.0/src/cairns/core/cairn.py +98 -0
  34. cairns-0.2.0/src/cairns/core/hash.py +178 -0
  35. cairns-0.2.0/src/cairns/core/lock.py +65 -0
  36. cairns-0.2.0/src/cairns/core/runtime.py +376 -0
  37. cairns-0.2.0/src/cairns/core/serial.py +144 -0
  38. cairns-0.2.0/src/cairns/core/sink.py +80 -0
  39. cairns-0.2.0/src/cairns/core/step.py +720 -0
  40. cairns-0.2.0/src/cairns/core/store.py +621 -0
  41. cairns-0.2.0/src/cairns/core/types.py +340 -0
  42. cairns-0.2.0/src/cairns/ext/__init__.py +0 -0
  43. cairns-0.2.0/src/cairns/ext/claude.py +244 -0
  44. cairns-0.2.0/src/cairns/interaction/__init__.py +250 -0
  45. cairns-0.2.0/src/cairns/patterns/__init__.py +58 -0
  46. cairns-0.2.0/src/cairns/patterns/semver.py +55 -0
  47. cairns-0.2.0/src/cairns/py.typed +0 -0
  48. cairns-0.2.0/src/cairns/run/__init__.py +271 -0
  49. cairns-0.2.0/src/cairns/run/gc.py +266 -0
  50. cairns-0.2.0/src/cairns/run/show.py +306 -0
  51. cairns-0.2.0/src/cairns/run/spans.py +183 -0
  52. cairns-0.2.0/src/cairns/testing.py +144 -0
  53. cairns-0.2.0/src/cairns/tui/__init__.py +31 -0
  54. cairns-0.2.0/src/cairns/tui/app.py +738 -0
  55. cairns-0.2.0/src/cairns/tui/messages.py +91 -0
  56. cairns-0.2.0/src/cairns/tui/render.py +43 -0
  57. cairns-0.2.0/src/cairns/tui/sinks.py +110 -0
  58. cairns-0.2.0/src/cairns/tui/widgets.py +144 -0
  59. cairns-0.2.0/tests/conftest.py +1 -0
  60. cairns-0.2.0/tests/test_core.py +696 -0
  61. cairns-0.2.0/tests/test_disk.py +432 -0
  62. cairns-0.2.0/tests/test_gc.py +241 -0
  63. cairns-0.2.0/tests/test_hash.py +522 -0
  64. cairns-0.2.0/tests/test_metrics.py +291 -0
  65. cairns-0.2.0/tests/test_patterns.py +276 -0
  66. cairns-0.2.0/tests/test_resume.py +143 -0
  67. cairns-0.2.0/tests/test_serial.py +120 -0
  68. cairns-0.2.0/uv.lock +444 -0
cairns-0.2.0/.envrc ADDED
@@ -0,0 +1 @@
1
+ use devenv
@@ -0,0 +1,6 @@
1
+ **/.devenv
2
+ .pytest_cache
3
+ .claude
4
+ **/__pycache__/
5
+ .cairn/
6
+ .env
@@ -0,0 +1,2 @@
1
+ # Automatically created by Grimp.
2
+ *
@@ -0,0 +1 @@
1
+ {"cairn.run.spans":[],"cairn.run.show":[["cairn.core",174,"from cairn.core import event_to_dict"],["cairn.run.gc",17,"from .gc import list_runs"],["cairn.core",16,"from cairn.core import Event"],["cairn.run.spans",18,"from .spans import SpanGraph"]],"cairn.tui.messages":[],"cairn.tui.app":[["cairn.core",21,"from cairn.core import CompositeSink, Handle, set_sink, set_store"],["cairn.tui.sinks",34,"from .sinks import TuiInteractionSink, TuiSink"],["cairn.interaction",22,"from cairn.interaction import set_interaction_sink"],["cairn.tui.render",33,"from .render import render_trace_text"],["cairn.core",574,"from cairn.core import reset_sink, reset_store"],["cairn.core",406,"from cairn.core import from_jsonable"],["cairn.tui.widgets",35,"from .widgets import ChoicePanel, ConfirmPanel"],["cairn.run.spans",24,"from cairn.run.spans import SpanGraph"],["cairn.interaction",575,"from cairn.interaction import reset_interaction_sink"],["cairn.run",23,"from cairn.run import RunInfo, RunManager, SymlinkTracker, list_runs"],["cairn.tui.messages",26,"from .messages import ("]],"cairn":[["cairn.run.gc",16,"from cairn.run import ("],["cairn.core",3,"from cairn.core import ("],["cairn.core.step",3,"from cairn.core import ("],["cairn.run",16,"from cairn.run import ("]],"cairn.cli":[["cairn.run",36,"from cairn.run import show_trace"],["cairn.tui",145,"from cairn.tui import browse"],["cairn.run",64,"from cairn.run import show_runs"],["cairn.run.gc",52,"from cairn.run import gc, list_runs"],["cairn.tui",128,"from cairn.tui import run_app"],["cairn.run",30,"from cairn.run import show_runs"],["cairn.run",149,"from cairn.run import show_runs"],["cairn.run",52,"from cairn.run import gc, list_runs"],["cairn.run",80,"from cairn.run import run as cairn_run"],["cairn.run",119,"from cairn.run import gc_outputs, list_runs, remove_run"],["cairn.run",43,"from cairn.run import show_output"]],"cairn.ext":[],"cairn.interaction":[["cairn.core.step",39,"from cairn.core import step"],["cairn.core.context",40,"from cairn.core.context import current_span"]],"cairn.core.hash":[["cairn.core.types",129,"from .types import StepInfo"]],"cairn.core.types":[["cairn.core.hash",16,"from .hash import compute_cairn_id, resolve_hashable"]],"cairn.tui.render":[["cairn.run.show",9,"from cairn.run.show import TRACE_RESERVED, format_cost"]],"cairn.core":[["cairn.core.step",8,"from .step import ("],["cairn.core.sink",45,"from .sink import CompositeSink, JSONLSink, event_to_dict"],["cairn.core.hash",30,"from .hash import ("],["cairn.core.store",46,"from .store import FileStore, MemoryStore, OverlayStore, Store, StoreStats"],["cairn.core.context",18,"from .context import ("],["cairn.core.serial",38,"from .serial import ("],["cairn.core.types",47,"from .types import CacheEntry, SpanMetrics, StepInfo, TaskSpan, TraceRecord"],["cairn.core.patterns",37,"from .patterns import rate_limited, replayable"]],"cairn.core.context":[["cairn.core.types",11,"from .types import TaskSpan"]],"cairn.core.lock":[],"cairn.tui":[["cairn.tui.app",18,"from .app import CairnApp"]],"cairn.core.step":[["cairn.core.types",20,"from .types import CacheEntry, SpanMetrics, StepInfo, TaskSpan, TraceRecord"],["cairn.core.store",12,"from .store import ("],["cairn.core.context",11,"from .context import current_span, emit_event, next_id"]],"cairn.core.testing":[["cairn.core",9,"from cairn.core import set_store"],["cairn.core.store",11,"from .store import MemoryStore"],["cairn.core.context",8,"from .context import Event, MemorySink, reset_id_counter, set_sink"],["cairn.core.context",131,"from .context import reset_sink"],["cairn.core.hash",10,"from .hash import clear_hash_funcs, set_hash_funcs"],["cairn.core.step",130,"from .step import reset_store"]],"cairn.tui.widgets":[],"cairn.tui.sinks":[["cairn.tui.messages",20,"from .messages import ("],["cairn.core",18,"from cairn.core import Event, event_to_dict"]],"cairn.run.gc":[["cairn.core.lock",15,"from cairn.core.lock import gc_exclusive as _gc_exclusive"]],"cairn.ext.claude":[["cairn",16,"from cairn import trace"]],"cairn.core.store":[["cairn.core.serial",20,"from .serial import from_jsonable, to_jsonable"],["cairn.core.types",21,"from .types import CacheEntry, TraceRecord"],["cairn.core.lock",19,"from .lock import store_shared"]],"cairn.run":[["cairn.core",175,"from cairn.core import reset_sink, reset_store # noqa: PLC0415"],["cairn.run.gc",185,"from .gc import ( # noqa: E402"],["cairn.run.show",193,"from .show import show_output, show_runs, show_trace # noqa: E402"],["cairn.run.spans",194,"from .spans import SpanGraph # noqa: E402"],["cairn.core",12,"from cairn.core import Event, FileStore, Handle, JSONLSink, OverlayStore, set_sink, set_store"]],"cairn.core.serial":[],"cairn.core.patterns":[["cairn.core.step",10,"from .step import Handle, cached_output, cached_tracing, step, trace"],["cairn.core.types",11,"from .types import StepInfo, TraceRecord"]],"cairn.core.sink":[["cairn.core.context",10,"from .context import Event"]]}
@@ -0,0 +1,3 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag automatically created by Grimp.
3
+ # For information about cache directory tags see https://bford.info/cachedir/
@@ -0,0 +1 @@
1
+ {"cairn.core.patterns": 1776711829.4129565, "cairn.tui.widgets": 1776725754.021825, "cairn.ext": 1777061802.32637, "cairn.run": 1777065258.432119, "cairn.run.show": 1777065290.855728, "cairn.tui": 1776729888.950072, "cairn.tui.sinks": 1776729888.9504905, "cairn.interaction": 1776730680.4906645, "cairn.core": 1777065234.2434435, "cairn.tui.render": 1776729888.9503968, "cairn.core.step": 1777068889.3958, "cairn.cli": 1776729888.9499216, "cairn.core.sink": 1777066204.0551448, "cairn.core.types": 1777064789.6603053, "cairn.run.gc": 1777065279.6702857, "cairn.run.spans": 1777065135.7066474, "cairn.tui.app": 1777066213.3082044, "cairn.core.serial": 1777064703.2457192, "cairn.core.lock": 1777062187.6831, "cairn": 1776697640.5956903, "cairn.tui.messages": 1776729888.950324, "cairn.ext.claude": 1777062502.825604, "cairn.core.hash": 1777064796.0770473, "cairn.core.store": 1777068851.1281378, "cairn.core.context": 1777066220.2757235, "cairn.core.testing": 1776689957.1186948}
@@ -0,0 +1 @@
1
+ {"cairns.run.spans": 1777908586.6772733, "cairns.core.serial": 1777908821.6975274, "cairns.run": 1777909145.351873, "cairns.ext.claude": 1777908738.108794, "cairns.core.runtime": 1777910918.4830177, "cairns.core.step": 1777910982.8763356, "cairns.tui": 1777910918.484366, "cairns.core.sink": 1777892323.3431523, "cairns.testing": 1777909069.6382208, "cairns.tui.widgets": 1776725754.021825, "cairns.core.lock": 1777910918.4838786, "cairns.patterns.semver": 1777911005.908118, "cairns.core.hash": 1777908821.7029088, "cairns.patterns": 1777908738.0626025, "cairns.core.types": 1777908236.6348233, "cairns.ext": 1777061802.32637, "cairns.core.store": 1777909027.1370761, "cairns.tui.app": 1777908738.0930882, "cairns": 1777908848.0384898, "cairns.cli": 1777911082.101685, "cairns.tui.messages": 1776729888.950324, "cairns.run.show": 1777909027.149721, "cairns.tui.render": 1777908738.0466282, "cairns.core.cairn": 1777910989.1823654, "cairns.interaction": 1777908738.1828606, "cairns.tui.sinks": 1777908738.0774064, "cairns.run.gc": 1777910918.4849072, "cairns.core": 1777910959.412853}
@@ -0,0 +1 @@
1
+ {"cairns.tui.app":[["cairns.core",407,"from cairns.core import from_jsonable"],["cairns.tui.messages",27,"from .messages import ("],["cairns.run",24,"from cairns.run import _make_run_dir, _update_latest # noqa: PLC2701"],["cairns.tui.widgets",36,"from .widgets import ChoicePanel, ConfirmPanel"],["cairns.run.spans",25,"from cairns.run.spans import SpanGraph"],["cairns.run",23,"from cairns.run import RunDirSink, RunInfo, list_runs"],["cairns.tui.render",34,"from .render import render_trace_text"],["cairns.core",21,"from cairns.core import CompositeSink, FileStore, Handle"],["cairns.tui.sinks",35,"from .sinks import TuiInteractionSink, TuiSink"],["cairns.core.runtime",22,"from cairns.core.runtime import Run"]],"cairns.core.step":[["cairns.core.runtime",11,"from .runtime import current_run, current_span, emit_event"],["cairns.core.cairn",672,"from cairns.core.cairn import Cairn # noqa: PLC0415"],["cairns.core.types",19,"from .types import Record, SpanMetrics, StepInfo, TaskSpan, TraceRecord"],["cairns.core.store",12,"from .store import ("]],"cairns.run.spans":[],"cairns.patterns":[["cairns.core.types",11,"from cairns.core.types import StepInfo, TraceRecord"],["cairns.core.step",10,"from cairns.core.step import Handle, cached_output, cached_tracing, step, trace"]],"cairns.interaction":[["cairns.core.step",29,"from cairns.core import step"],["cairns.core.runtime",30,"from cairns.core.runtime import InteractionSink, current_run, current_span"]],"cairns.ext.claude":[["cairns",16,"from cairns import trace"]],"cairns.core.serial":[["cairns.core.runtime",44,"from .runtime import active_serializers # noqa: PLC0415"]],"cairns.core.types":[["cairns.core.hash",16,"from .hash import compute_cairn_id, resolve_hashable"]],"cairns.patterns.semver":[["cairns.core",23,"from cairns.core import Cairn, Record"]],"cairns.core.store":[["cairns.core.lock",19,"from .lock import store_shared"],["cairns.core.serial",20,"from .serial import from_jsonable, to_jsonable"],["cairns.core.types",21,"from .types import Record, TraceRecord"]],"cairns.tui.widgets":[],"cairns.tui.messages":[],"cairns":[["cairns.core",3,"from cairns.core import ("],["cairns.core.step",3,"from cairns.core import ("],["cairns.run.gc",17,"from cairns.run import ("],["cairns.run",17,"from cairns.run import ("],["cairns.patterns",16,"from cairns.patterns import rate_limited, replayable"],["cairns.core.cairn",3,"from cairns.core import ("]],"cairns.core.lock":[],"cairns.run.show":[["cairns.core",174,"from cairns.core import event_to_dict"],["cairns.run.gc",17,"from .gc import list_runs"],["cairns.core",16,"from cairns.core import Event"],["cairns.run.spans",18,"from .spans import SpanGraph"]],"cairns.core":[["cairns.patterns",33,"from cairns.patterns import rate_limited, replayable"],["cairns.core.cairn",28,"from .cairn import Cairn, cairn # noqa: F401"],["cairns.core.runtime",15,"from .runtime import ("],["cairns.core.sink",39,"from .sink import CompositeSink, JSONLSink, event_to_dict"],["cairns.core.store",40,"from .store import FileStore, MemoryStore, OverlayStore, Store, StoreStats"],["cairns.core.hash",29,"from .hash import ("],["cairns.core.serial",34,"from .serial import ("],["cairns.core.types",41,"from .types import Record, SpanMetrics, StepInfo, TaskSpan, TraceRecord"],["cairns.core.step",8,"from .step import ("]],"cairns.run.gc":[["cairns.core.lock",15,"from cairns.core.lock import gc_exclusive as _gc_exclusive"]],"cairns.ext":[],"cairns.core.sink":[["cairns.core.runtime",10,"from .runtime import Event"]],"cairns.tui.sinks":[["cairns.core",18,"from cairns.core import Event, event_to_dict"],["cairns.tui.messages",20,"from .messages import ("]],"cairns.run":[["cairns.core",11,"from cairns.core import Event, Handle, JSONLSink, OverlayStore"],["cairns.run.gc",246,"from .gc import ( # noqa: E402"],["cairns.core.runtime",12,"from cairns.core.runtime import ("],["cairns.run.show",254,"from .show import show_output, show_runs, show_trace # noqa: E402"],["cairns.run.spans",255,"from .spans import SpanGraph # noqa: E402"]],"cairns.core.cairn":[["cairns.core.store",25,"from .store import Store"],["cairns.core.runtime",21,"from .runtime import current_run, current_span"],["cairns.core.types",22,"from .types import Record"]],"cairns.tui.render":[["cairns.run.show",9,"from cairns.run.show import TRACE_RESERVED, format_cost"]],"cairns.testing":[["cairns.core.runtime",8,"from cairns.core.runtime import Runtime, Event, MemorySink, Run"],["cairns.core.store",9,"from cairns.core.store import MemoryStore"]],"cairns.core.hash":[["cairns.core.runtime",90,"from .runtime import active_hash_funcs # noqa: PLC0415"],["cairns.core.types",143,"from .types import StepInfo"]],"cairns.tui":[["cairns.tui.app",18,"from .app import CairnsApp"]],"cairns.core.runtime":[["cairns.run",245,"from cairns.run import arun as _arun # noqa: PLC0415"],["cairns.testing",259,"from cairns.testing import Harness # noqa: PLC0415"],["cairns.core.hash",178,"from .hash import install_defaults as install_hash_defaults # noqa: PLC0415"],["cairns.core.types",33,"from .types import TaskSpan"],["cairns.core.serial",179,"from .serial import install_defaults as install_serial_defaults # noqa: PLC0415"],["cairns.core.store",170,"from .store import FileStore # noqa: PLC0415"],["cairns.core.serial",36,"from .serial import Serializer"],["cairns.core.store",37,"from .store import Store"],["cairns.run",217,"from cairns.run import run as _execute # noqa: PLC0415"]],"cairns.cli":[["cairns.run",80,"from cairns.run import run as cairn_run"],["cairns.run",30,"from cairns.run import show_runs"],["cairns.run",43,"from cairns.run import show_output"],["cairns.run",36,"from cairns.run import show_trace"],["cairns.run",149,"from cairns.run import show_runs"],["cairns.run",119,"from cairns.run import gc_outputs, list_runs, remove_run"],["cairns.run",52,"from cairns.run import gc, list_runs"],["cairns.tui",145,"from cairns.tui import browse"],["cairns.tui",128,"from cairns.tui import run_app"],["cairns.run",64,"from cairns.run import show_runs"],["cairns.run.gc",52,"from cairns.run import gc, list_runs"]]}
cairns-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mathijs Henquet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cairns-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: cairns
3
+ Version: 0.2.0
4
+ Summary: Compute graph orchestration with caching and observability
5
+ Project-URL: Homepage, https://github.com/mathijshenquet/cairns
6
+ Project-URL: Repository, https://github.com/mathijshenquet/cairns
7
+ Project-URL: Issues, https://github.com/mathijshenquet/cairns/issues
8
+ Author-email: Mathijs Henquet <mathijs.henquet@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,async,caching,compute-graph,llm,observability,pipeline,tracing,workflow
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: System :: Distributed Computing
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Provides-Extra: dev
24
+ Requires-Dist: import-linter>=2.0; extra == 'dev'
25
+ Requires-Dist: pydantic>=2.0; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: rich>=13.0; extra == 'dev'
29
+ Requires-Dist: textual>=3.0; extra == 'dev'
30
+ Provides-Extra: full
31
+ Requires-Dist: pydantic>=2.0; extra == 'full'
32
+ Requires-Dist: rich>=13.0; extra == 'full'
33
+ Requires-Dist: textual>=3.0; extra == 'full'
34
+ Provides-Extra: pydantic
35
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
36
+ Provides-Extra: semver
37
+ Requires-Dist: semver>=3.0; extra == 'semver'
38
+ Provides-Extra: tui
39
+ Requires-Dist: rich>=13.0; extra == 'tui'
40
+ Requires-Dist: textual>=3.0; extra == 'tui'
41
+ Description-Content-Type: text/markdown
42
+
43
+ <p align="center">
44
+ <img src="https://raw.githubusercontent.com/mathijshenquet/cairns/main/docs/logo.jpg" alt="Cairn" width="600">
45
+ </p>
46
+
47
+ # Cairns
48
+
49
+ **A microframework for compute graphs with caching, tracing, and replay.**
50
+
51
+ Think *PyTorch for agent pipelines* — though nothing about it is agent-specific.
52
+ You write ordinary async Python; a `@step` decorator turns each function into a
53
+ tracked, and optionally cached node in a graph that emerges from execution instead of being
54
+ declared up front.
55
+
56
+ > **Alpha.** Public API names, the on-disk cache format, and the higher-order
57
+ > wrappers may still change between minor versions. Pin a version if you
58
+ > depend on it.
59
+
60
+ ## Why
61
+
62
+ Declarative graph frameworks (LangGraph, CrewAI, Airflow-style DAGs) force you
63
+ into their node/edge DSL. Cairn goes the other way: the graph **is** your code,
64
+ the framework just instruments it. From that you get:
65
+
66
+ - **Caching** keyed on `(function identity, body version, resolved args)` — change
67
+ one function, only its downstream re-executes.
68
+ - **Tracing** — live, structured event log; a built-in TUI renders it.
69
+ - **Resume** — a failed pipeline reruns from the last successful step.
70
+ - **Replay** — cached runs can replay with original timing, indistinguishable
71
+ from a live execution.
72
+ - **Human-in-the-loop** as a regular async step (`await_input(...)`).
73
+
74
+ Works for LLM pipelines, scrapers, ETL, long-running research — anything that
75
+ fits into mostly-pure async functions.
76
+
77
+ ## Install
78
+
79
+ ```sh
80
+ pip install cairns[tui] # TUI is worth having
81
+ pip install cairns[full] # TUI + pydantic hashing
82
+ ```
83
+
84
+ Or with `uv`:
85
+
86
+ ```sh
87
+ uv add cairns --extra tui
88
+ ```
89
+
90
+ Requires Python 3.12+.
91
+
92
+ ## Hello world
93
+
94
+ ```python
95
+ import asyncio
96
+ from cairns import step, run, trace
97
+
98
+ @step(memo=True)
99
+ async def fetch(url: str) -> str:
100
+ trace("fetching", state="running")
101
+ await asyncio.sleep(0.2) # pretend HTTP
102
+ return f"<html>{url}</html>"
103
+
104
+ @step
105
+ async def extract(html: str) -> int:
106
+ return len(html)
107
+
108
+ @step
109
+ async def pipeline(urls: list[str]) -> list[int]:
110
+ pages = [fetch(u) for u in urls] # returns Handles; runs concurrently
111
+ return [await extract(p) for p in pages] # pages are awaited inside extract
112
+
113
+ run(pipeline, store_path=".cairns",
114
+ args=(["https://a", "https://b", "https://c"],))
115
+ ```
116
+
117
+ Run it twice. The second run is instant — every `@step(memo=True)` result is
118
+ looked up by cache key. Edit the body of `extract`, rerun: only `extract`
119
+ re-executes, fetches are cache hits.
120
+
121
+ ## CLI
122
+
123
+ ```sh
124
+ cairns examples/research_fake_llm.py # run the pipeline, opens TUI if installed
125
+ cairns examples/research_fake_llm.py slow # run the `slow` entry point
126
+ cairns examples/research_fake_llm.py -f # clear this entry's cache, then run
127
+ cairns # interactive run browser over past runs
128
+ cairns list # flat list of runs
129
+ cairns show [RUN_ID] # print a trace (latest if omitted)
130
+ cairns gc [--before YYYY-MM-DD] # garbage-collect old runs
131
+ ```
132
+
133
+ Default store is `./.cairns/`. Override with `--store PATH` (or `-s`). Entry
134
+ points default to a function named `main`; pass a second positional arg to
135
+ pick another, e.g. `cairns script.py my_pipeline`.
136
+
137
+ ## Examples
138
+
139
+ Each example runs standalone with `python examples/<name>.py`, or through the
140
+ CLI with `cairns examples/<name>.py` for the TUI.
141
+
142
+ | Example | What it shows |
143
+ |---------|---------------|
144
+ | [`scraper.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/scraper.py) | Fan-out + chain, non-AI, fully mocked. Good first look. |
145
+ | [`failure_resume.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/failure_resume.py) | A step fails on item 3; rerun resumes from cache. |
146
+ | [`research_fake_llm.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/research_fake_llm.py) | Fan-out across N subjects, retry loop, rate limiting, simulated 20% API failure rate. No API key needed. |
147
+ | [`hitl.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/hitl.py) | `await_input` inside a step — TUI input widget, stdin fallback. |
148
+ | [`research_haiku.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/research_haiku.py) + [`claude.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/claude.py) | Live research over real AI companies via the `claude` CLI (Haiku). Cached by ISO week — re-runs within the week are free. |
149
+
150
+ ## What you'll see
151
+
152
+ With `cairns[tui]` installed, the CLI opens a live span tree: each `@step`
153
+ invocation is a row, child steps indent, `trace(...)` calls attach as
154
+ annotations, and `cost={...}` kwargs get summed up the tree. Failures colour
155
+ red, running steps pulse, completed steps show wall time + own time (excluding
156
+ waits on children).
157
+
158
+ Without the TUI, the same events stream to `.cairns/runs/{entry}-{ts}/trace.jsonl`
159
+ and you can read them with `cairns show`.
160
+
161
+ ## Docs
162
+
163
+ - [`docs/motivation.md`](https://github.com/mathijshenquet/cairns/blob/main/docs/motivation.md) — the problem and the analogy to PyTorch.
164
+ - [`docs/design.md`](https://github.com/mathijshenquet/cairns/blob/main/docs/design.md) — all primitives, event log, stores, plugin points.
165
+ - [`docs/patterns.md`](https://github.com/mathijshenquet/cairns/blob/main/docs/patterns.md) — comparison against Prefect, LangGraph, Temporal, Flyte, CrewAI across seven patterns.
166
+
167
+ ## Status
168
+
169
+ Alpha, but the core works:
170
+
171
+ - `@step`, `Handle`, `trace`, `cached_output/tracing`, `replayable`, `rate_limited`, `await_input` all shipped.
172
+ - File-backed content-addressed store, JSONL trace sink, symlinked run layout, GC.
173
+ - Live TUI for span-tree viewing.
174
+ - 110 tests, covering core + hashing + disk + resume + GC + metrics + patterns.
175
+
176
+ Possible future work: a web UI, distributed execution, distributed cache store.
177
+
178
+ Feedback and breakage reports welcome via issues.
cairns-0.2.0/README.md ADDED
@@ -0,0 +1,136 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mathijshenquet/cairns/main/docs/logo.jpg" alt="Cairn" width="600">
3
+ </p>
4
+
5
+ # Cairns
6
+
7
+ **A microframework for compute graphs with caching, tracing, and replay.**
8
+
9
+ Think *PyTorch for agent pipelines* — though nothing about it is agent-specific.
10
+ You write ordinary async Python; a `@step` decorator turns each function into a
11
+ tracked, and optionally cached node in a graph that emerges from execution instead of being
12
+ declared up front.
13
+
14
+ > **Alpha.** Public API names, the on-disk cache format, and the higher-order
15
+ > wrappers may still change between minor versions. Pin a version if you
16
+ > depend on it.
17
+
18
+ ## Why
19
+
20
+ Declarative graph frameworks (LangGraph, CrewAI, Airflow-style DAGs) force you
21
+ into their node/edge DSL. Cairn goes the other way: the graph **is** your code,
22
+ the framework just instruments it. From that you get:
23
+
24
+ - **Caching** keyed on `(function identity, body version, resolved args)` — change
25
+ one function, only its downstream re-executes.
26
+ - **Tracing** — live, structured event log; a built-in TUI renders it.
27
+ - **Resume** — a failed pipeline reruns from the last successful step.
28
+ - **Replay** — cached runs can replay with original timing, indistinguishable
29
+ from a live execution.
30
+ - **Human-in-the-loop** as a regular async step (`await_input(...)`).
31
+
32
+ Works for LLM pipelines, scrapers, ETL, long-running research — anything that
33
+ fits into mostly-pure async functions.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install cairns[tui] # TUI is worth having
39
+ pip install cairns[full] # TUI + pydantic hashing
40
+ ```
41
+
42
+ Or with `uv`:
43
+
44
+ ```sh
45
+ uv add cairns --extra tui
46
+ ```
47
+
48
+ Requires Python 3.12+.
49
+
50
+ ## Hello world
51
+
52
+ ```python
53
+ import asyncio
54
+ from cairns import step, run, trace
55
+
56
+ @step(memo=True)
57
+ async def fetch(url: str) -> str:
58
+ trace("fetching", state="running")
59
+ await asyncio.sleep(0.2) # pretend HTTP
60
+ return f"<html>{url}</html>"
61
+
62
+ @step
63
+ async def extract(html: str) -> int:
64
+ return len(html)
65
+
66
+ @step
67
+ async def pipeline(urls: list[str]) -> list[int]:
68
+ pages = [fetch(u) for u in urls] # returns Handles; runs concurrently
69
+ return [await extract(p) for p in pages] # pages are awaited inside extract
70
+
71
+ run(pipeline, store_path=".cairns",
72
+ args=(["https://a", "https://b", "https://c"],))
73
+ ```
74
+
75
+ Run it twice. The second run is instant — every `@step(memo=True)` result is
76
+ looked up by cache key. Edit the body of `extract`, rerun: only `extract`
77
+ re-executes, fetches are cache hits.
78
+
79
+ ## CLI
80
+
81
+ ```sh
82
+ cairns examples/research_fake_llm.py # run the pipeline, opens TUI if installed
83
+ cairns examples/research_fake_llm.py slow # run the `slow` entry point
84
+ cairns examples/research_fake_llm.py -f # clear this entry's cache, then run
85
+ cairns # interactive run browser over past runs
86
+ cairns list # flat list of runs
87
+ cairns show [RUN_ID] # print a trace (latest if omitted)
88
+ cairns gc [--before YYYY-MM-DD] # garbage-collect old runs
89
+ ```
90
+
91
+ Default store is `./.cairns/`. Override with `--store PATH` (or `-s`). Entry
92
+ points default to a function named `main`; pass a second positional arg to
93
+ pick another, e.g. `cairns script.py my_pipeline`.
94
+
95
+ ## Examples
96
+
97
+ Each example runs standalone with `python examples/<name>.py`, or through the
98
+ CLI with `cairns examples/<name>.py` for the TUI.
99
+
100
+ | Example | What it shows |
101
+ |---------|---------------|
102
+ | [`scraper.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/scraper.py) | Fan-out + chain, non-AI, fully mocked. Good first look. |
103
+ | [`failure_resume.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/failure_resume.py) | A step fails on item 3; rerun resumes from cache. |
104
+ | [`research_fake_llm.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/research_fake_llm.py) | Fan-out across N subjects, retry loop, rate limiting, simulated 20% API failure rate. No API key needed. |
105
+ | [`hitl.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/hitl.py) | `await_input` inside a step — TUI input widget, stdin fallback. |
106
+ | [`research_haiku.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/research_haiku.py) + [`claude.py`](https://github.com/mathijshenquet/cairns/blob/main/examples/claude.py) | Live research over real AI companies via the `claude` CLI (Haiku). Cached by ISO week — re-runs within the week are free. |
107
+
108
+ ## What you'll see
109
+
110
+ With `cairns[tui]` installed, the CLI opens a live span tree: each `@step`
111
+ invocation is a row, child steps indent, `trace(...)` calls attach as
112
+ annotations, and `cost={...}` kwargs get summed up the tree. Failures colour
113
+ red, running steps pulse, completed steps show wall time + own time (excluding
114
+ waits on children).
115
+
116
+ Without the TUI, the same events stream to `.cairns/runs/{entry}-{ts}/trace.jsonl`
117
+ and you can read them with `cairns show`.
118
+
119
+ ## Docs
120
+
121
+ - [`docs/motivation.md`](https://github.com/mathijshenquet/cairns/blob/main/docs/motivation.md) — the problem and the analogy to PyTorch.
122
+ - [`docs/design.md`](https://github.com/mathijshenquet/cairns/blob/main/docs/design.md) — all primitives, event log, stores, plugin points.
123
+ - [`docs/patterns.md`](https://github.com/mathijshenquet/cairns/blob/main/docs/patterns.md) — comparison against Prefect, LangGraph, Temporal, Flyte, CrewAI across seven patterns.
124
+
125
+ ## Status
126
+
127
+ Alpha, but the core works:
128
+
129
+ - `@step`, `Handle`, `trace`, `cached_output/tracing`, `replayable`, `rate_limited`, `await_input` all shipped.
130
+ - File-backed content-addressed store, JSONL trace sink, symlinked run layout, GC.
131
+ - Live TUI for span-tree viewing.
132
+ - 110 tests, covering core + hashing + disk + resume + GC + metrics + patterns.
133
+
134
+ Possible future work: a web UI, distributed execution, distributed cache store.
135
+
136
+ Feedback and breakage reports welcome via issues.
@@ -0,0 +1,34 @@
1
+ version: "3"
2
+
3
+ dotenv: [".env"]
4
+
5
+ tasks:
6
+ build:
7
+ desc: Build sdist + wheel into dist/
8
+ cmds:
9
+ - rm -rf dist/
10
+ - uv build
11
+
12
+ check:
13
+ desc: Validate built artifacts (twine check)
14
+ deps: [build]
15
+ cmds:
16
+ - uvx twine check dist/*
17
+
18
+ publish:
19
+ desc: Publish to PyPI. Reads $PYPI_TOKEN from env (loaded from .env via devenv).
20
+ deps: [check]
21
+ preconditions:
22
+ - sh: 'test -n "$PYPI_TOKEN"'
23
+ msg: "PYPI_TOKEN is not set. Add it to .env (devenv auto-loads dotenv)."
24
+ cmds:
25
+ - UV_PUBLISH_TOKEN="$PYPI_TOKEN" uv publish
26
+
27
+ publish:test:
28
+ desc: Publish to TestPyPI. Reads $PYPI_TEST_TOKEN (or $PYPI_TOKEN as fallback).
29
+ deps: [check]
30
+ preconditions:
31
+ - sh: 'test -n "$PYPI_TEST_TOKEN" -o -n "$PYPI_TOKEN"'
32
+ msg: "Set PYPI_TEST_TOKEN (or PYPI_TOKEN) in .env."
33
+ cmds:
34
+ - UV_PUBLISH_TOKEN="${PYPI_TEST_TOKEN:-$PYPI_TOKEN}" uv publish --publish-url https://test.pypi.org/legacy/
@@ -0,0 +1,103 @@
1
+ {
2
+ "nodes": {
3
+ "devenv": {
4
+ "locked": {
5
+ "dir": "src/modules",
6
+ "lastModified": 1776718099,
7
+ "narHash": "sha256-JAR6x8Au4xxk6X7ijhlYETQg0F0OS0ihZ5ARiguDclc=",
8
+ "owner": "cachix",
9
+ "repo": "devenv",
10
+ "rev": "034b677ee035a29a077ecbaadfb2908719272919",
11
+ "type": "github"
12
+ },
13
+ "original": {
14
+ "dir": "src/modules",
15
+ "owner": "cachix",
16
+ "repo": "devenv",
17
+ "type": "github"
18
+ }
19
+ },
20
+ "flake-compat": {
21
+ "flake": false,
22
+ "locked": {
23
+ "lastModified": 1767039857,
24
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
25
+ "owner": "edolstra",
26
+ "repo": "flake-compat",
27
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "edolstra",
32
+ "repo": "flake-compat",
33
+ "type": "github"
34
+ }
35
+ },
36
+ "nixpkgs": {
37
+ "inputs": {
38
+ "nixpkgs-src": "nixpkgs-src"
39
+ },
40
+ "locked": {
41
+ "lastModified": 1776348333,
42
+ "narHash": "sha256-ZyrYhlLGGuN5ieW7pE65meJJYnNz5el9vJtGBEJyDMQ=",
43
+ "owner": "cachix",
44
+ "repo": "devenv-nixpkgs",
45
+ "rev": "efff47329167854ce48541c7ef731bf120753c7e",
46
+ "type": "github"
47
+ },
48
+ "original": {
49
+ "owner": "cachix",
50
+ "ref": "rolling",
51
+ "repo": "devenv-nixpkgs",
52
+ "type": "github"
53
+ }
54
+ },
55
+ "nixpkgs-python": {
56
+ "inputs": {
57
+ "flake-compat": "flake-compat",
58
+ "nixpkgs": [
59
+ "nixpkgs"
60
+ ]
61
+ },
62
+ "locked": {
63
+ "lastModified": 1774026014,
64
+ "narHash": "sha256-UBBQYhyAKayDCi6iCIKShQXWRvwyj5omLPOuSeVjtOY=",
65
+ "owner": "cachix",
66
+ "repo": "nixpkgs-python",
67
+ "rev": "8b6d4103312761d4144b7bf9aebcc2f394b7e325",
68
+ "type": "github"
69
+ },
70
+ "original": {
71
+ "owner": "cachix",
72
+ "repo": "nixpkgs-python",
73
+ "type": "github"
74
+ }
75
+ },
76
+ "nixpkgs-src": {
77
+ "flake": false,
78
+ "locked": {
79
+ "lastModified": 1775888245,
80
+ "narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",
81
+ "owner": "NixOS",
82
+ "repo": "nixpkgs",
83
+ "rev": "13043924aaa7375ce482ebe2494338e058282925",
84
+ "type": "github"
85
+ },
86
+ "original": {
87
+ "owner": "NixOS",
88
+ "ref": "nixpkgs-unstable",
89
+ "repo": "nixpkgs",
90
+ "type": "github"
91
+ }
92
+ },
93
+ "root": {
94
+ "inputs": {
95
+ "devenv": "devenv",
96
+ "nixpkgs": "nixpkgs",
97
+ "nixpkgs-python": "nixpkgs-python"
98
+ }
99
+ }
100
+ },
101
+ "root": "root",
102
+ "version": 7
103
+ }
@@ -0,0 +1,21 @@
1
+ { pkgs, config, ... }:
2
+
3
+ {
4
+ dotenv.enable = true;
5
+
6
+ languages.python = {
7
+ enable = true;
8
+ uv = {
9
+ enable = true;
10
+ sync.enable = true;
11
+ };
12
+ };
13
+
14
+ packages = [
15
+ pkgs.pyright
16
+ ];
17
+
18
+ enterShell = ''
19
+ export PATH="${config.devenv.root}/.devenv/state/venv/bin:$PATH"
20
+ '';
21
+ }
@@ -0,0 +1,6 @@
1
+ inputs:
2
+ nixpkgs-python:
3
+ url: github:cachix/nixpkgs-python
4
+ inputs:
5
+ nixpkgs:
6
+ follows: nixpkgs