loomcache 1.0.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.
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Run tests
24
+ run: pytest -v
25
+ - name: Run standalone demo (smoke test)
26
+ run: python examples/standalone_demo.py
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .venv/
9
+ venv/
10
+ .env
11
+
12
+ # Loom's own local cache/run output (never commit these)
13
+ .loom_cache/
14
+ .loom_runs/
15
+
16
+ .DS_Store
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+
4
+ ## [1.0.0] - 2026-08-30
5
+ ### Added
6
+ - Initial release: content-addressed step caching, Run/fork/diff, CLI.
7
+ - Fixed - Corrected Homepage/Repository URLs in package metadata.
@@ -0,0 +1,36 @@
1
+ # Contributing to Loom
2
+
3
+ Thanks for considering a contribution — this is a young project and
4
+ there's a lot of surface area to help with (see the Roadmap in the
5
+ README for ideas).
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ git clone https://github.com/SanjayDey786/loomtrace.git
11
+ cd loomtrace
12
+ pip install -e ".[dev]"
13
+ pytest
14
+ ```
15
+
16
+ ## Guidelines
17
+
18
+ - Keep the core (`loom/`) dependency-free. Optional integrations
19
+ (Redis cache backend, LangChain adapter, etc.) should be optional
20
+ extras, not hard dependencies.
21
+ - Every new behavior needs a test in `tests/`. `pytest` should pass
22
+ with zero warnings before you open a PR.
23
+ - If you change hashing behavior (`loom/hashing.py`) or the
24
+ `Node`/`Run` schema (`loom/run.py`), call it out explicitly in your
25
+ PR description — it can silently invalidate everyone's existing
26
+ caches and saved run files, which is a breaking change even if no
27
+ public function signature changed.
28
+ - Favor small, focused PRs over large ones. If you want to work on
29
+ something roadmap-sized (remote cache backend, LangChain adapter,
30
+ UI), open an issue first to discuss the design.
31
+
32
+ ## Reporting bugs / requesting features
33
+
34
+ Open a GitHub issue with a minimal reproduction where possible. For
35
+ bugs involving caching behavior specifically, include the two node
36
+ hashes you'd expect to be equal/different and why.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanjay Dey
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.
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.5
2
+ Name: loomcache
3
+ Version: 1.0.0
4
+ Summary: Content-addressed, deterministic execution engine for agent AI workflows.
5
+ Project-URL: Homepage, https://github.com/SanjayDey786/loomcache
6
+ Project-URL: Repository, https://github.com/SanjayDey786/loomcache
7
+ Project-URL: Issues, https://github.com/SanjayDey786/loomcache/issues
8
+ Author-email: Sanjay Dey <deysanjay30@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,caching,dag,llm,memoization,orchestration,reproducibility
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: all
25
+ Requires-Dist: boto3>=1.26.0; extra == 'all'
26
+ Requires-Dist: flask>=2.0.0; extra == 'all'
27
+ Requires-Dist: langchain-core>=0.1.0; extra == 'all'
28
+ Requires-Dist: redis>=4.5.0; extra == 'all'
29
+ Provides-Extra: anthropic
30
+ Requires-Dist: anthropic>=0.30.0; extra == 'anthropic'
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
33
+ Requires-Dist: pytest>=7.0; extra == 'dev'
34
+ Provides-Extra: langchain
35
+ Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
36
+ Provides-Extra: redis
37
+ Requires-Dist: redis>=4.5.0; extra == 'redis'
38
+ Provides-Extra: s3
39
+ Requires-Dist: boto3>=1.26.0; extra == 's3'
40
+ Provides-Extra: web
41
+ Requires-Dist: flask>=2.0.0; extra == 'web'
42
+ Description-Content-Type: text/markdown
43
+
44
+ [![PyPI version](https://badge.fury.io/py/loomcache.svg)](https://pypi.org/project/loomcache/)
45
+ # Loom
46
+
47
+ **The smallest tool that gives any Python script content‑addressed caching, partial re‑execution, and node‑level diffing — without an orchestration platform.**
48
+
49
+ Loom treats every function call in your pipeline the way Bazel treats a build target or Git treats a commit: as a hashed, content‑addressed node in a dependency graph. Change one prompt buried deep in a pipeline, and Loom re‑runs **only** that step and everything downstream of it — not the whole pipeline.
50
+
51
+
52
+ ## Table of Contents
53
+
54
+ - [What Loom Solves](#what-loom-solves)
55
+ - [How It Works](#how-it-works)
56
+ - [Quick Start](#quick-start)
57
+ - [Key Features](#key-features)
58
+ - [Core Caching & Execution](#core-caching--execution)
59
+ - [Remote Cache Backends](#remote-cache-backends-s3--redis)
60
+ - [LangChain / LangGraph Adapter](#langchain--langgraph-adapter)
61
+ - [Web UI](#web-ui)
62
+ - [Async Steps & Concurrency](#async-steps--concurrency)
63
+ - [CLI Reference](#cli-reference)
64
+ - [Installation](#installation)
65
+ - [Comparison with Existing Tools](#comparison-with-existing-tools)
66
+ - [Limitations (Read This)](#limitations-read-this)
67
+ - [Roadmap Status](#roadmap-status)
68
+ - [Contributing & Tests](#contributing--tests)
69
+ - [License](#license)
70
+
71
+ ---
72
+
73
+ ## What Loom Solves
74
+
75
+ If you’re hand‑rolling an agent script — not running it through a pipeline platform — you’ve likely faced these problems:
76
+
77
+ - Tweaking **one** prompt or **one** tool halfway through forces you to re‑run the **entire** pipeline — burning tokens, money, and wall‑clock time.
78
+ - When a pipeline’s output changes between two runs, you have no way to see **exactly where** the two runs diverged — you’re left diffing final text blobs and guessing.
79
+ - Debugging means adding print statements and re‑running the whole pipeline again, and again, and again.
80
+
81
+ **Loom solves this** by:
82
+
83
+ 1. **Hashing** every step’s source code + arguments (and upstream node hashes) → deterministic cache keys.
84
+ 2. **Caching** step outputs on disk (or S3/Redis) — identical calls return instantly.
85
+ 3. **Recording** every run as a list of nodes, so you can `diff` two runs node‑by‑node.
86
+ 4. **Forking** a run with a new input — only the changed step and its downstream steps re‑execute.
87
+
88
+ All of this works with **plain Python functions** – no special pipeline declarations, no extra infrastructure.
89
+
90
+ ---
91
+
92
+ ## How It Works
93
+
94
+ Every `@loom.step` call is hashed from:
95
+
96
+ 1. **The source code of the step function itself** — so editing a prompt template inside the function body invalidates the cache automatically.
97
+ 2. **Its arguments** — either their content hash, or, if an argument is itself the traced output of an upstream step, that step’s node hash. This turns a plain chain of Python function calls into a real hashable dependency graph — without any manual wiring.
98
+
99
+ ```text
100
+ user code Loom
101
+ --------- ----
102
+ @loom.step
103
+ def plan(q): ─────► 1. hash(source(plan) + q)
104
+ ... 2. cache lookup
105
+
106
+ hit ◄──┴──► miss
107
+ │ │
108
+ return cached execute plan(q)
109
+ output cache + record
110
+ │ │
111
+ └─────┬──────┘
112
+
113
+ tagged output (carries node hash)
114
+
115
+ passed into the next @loom.step call
116
+
117
+ hash includes the UPSTREAM node hash
118
+ (so changing `plan` invalidates every-
119
+ thing downstream of it automatically)
120
+
121
+
122
+ ---
123
+
124
+ ## Quick Start
125
+
126
+ ```bash
127
+ pip install loomtrace
128
+
129
+ ```
130
+
131
+ ```python
132
+ import loom
133
+
134
+ @loom.step
135
+ def plan(query: str) -> str:
136
+ return llm.call(f"Plan: {query}")
137
+
138
+ @loom.step
139
+ def execute(plan: str) -> str:
140
+ return tool.run(plan)
141
+
142
+ with loom.Run("research-agent") as run:
143
+ p = plan("find competitors of X")
144
+ result = execute(p)
145
+
146
+ run.save()
147
+
148
+ ```
149
+
150
+ Run that pipeline again unchanged — every step is served from cache in milliseconds. Change `plan`’s prompt or the input — only `plan` and its downstream steps re‑run.
151
+
152
+ ---
153
+
154
+ ## Key Features
155
+
156
+ ### Core Caching & Execution
157
+
158
+ * `@loom.step` – caches any function.
159
+ * `loom.Run(name)` – context manager; records every step call.
160
+ * `run.save()` / `Run.load(path)` – persist/restore runs as JSON.
161
+ * `run.fork(pipeline_fn, **kwargs)` – re‑run with new inputs; unchanged steps are cached.
162
+ * `loom.diff_runs(a, b)` / `loom.first_divergence(a, b)` – node‑by‑node diff.
163
+ * `loom.DiskCache(root=...)` – default local cache; subclass `loom.Cache` for other backends.
164
+
165
+ ### Remote Cache Backends (S3 / Redis)
166
+
167
+ ```python
168
+ from loom import S3Cache, RedisCache
169
+
170
+ # S3
171
+ cache = S3Cache(bucket="my-bucket", prefix="loom/")
172
+
173
+ # Redis
174
+ cache = RedisCache(url="redis://localhost:6379/0", key_prefix="loom:")
175
+
176
+ with loom.Run("pipeline", cache=cache) as run:
177
+ ...
178
+
179
+ ```
180
+
181
+ ### LangChain / LangGraph Adapter
182
+
183
+ ```python
184
+ from loom.langchain import wrap_runnable
185
+ from langchain.chains import LLMChain
186
+
187
+ chain = LLMChain(...)
188
+ cached_chain = wrap_runnable(chain, name="my_chain")
189
+
190
+ with loom.Run("lc_run") as run:
191
+ result = cached_chain.invoke({"input": "Hello"})
192
+
193
+ ```
194
+
195
+ ### Web UI
196
+
197
+ ```bash
198
+ loom web --runs-dir .loom_runs --port 5000
199
+
200
+ ```
201
+
202
+ Then open `http://localhost:5000` to browse runs, inspect nodes, and diff runs visually.
203
+
204
+ ### Async Steps & Concurrency
205
+
206
+ ```python
207
+ import asyncio
208
+ import loom
209
+
210
+ @loom.async_step
211
+ async def fetch_data(query: str) -> str:
212
+ await asyncio.sleep(0.1)
213
+ return f"Data for {query}"
214
+
215
+ async def main():
216
+ async with loom.AsyncRun("async_demo") as run:
217
+ results = await loom.gather(
218
+ fetch_data("A"),
219
+ fetch_data("B")
220
+ )
221
+ run.save()
222
+
223
+ asyncio.run(main())
224
+
225
+ ```
226
+
227
+ ---
228
+
229
+ ## CLI Reference
230
+
231
+ | Command | Description |
232
+ | --- | --- |
233
+ | `loom show <run.json>` | List all nodes in a run |
234
+ | `loom diff <a.json> <b.json>` | Node‑by‑node diff |
235
+ | `loom stats <run.json>` | Cache hit rate and timing |
236
+ | `loom web` | Launch the web UI |
237
+
238
+ ---
239
+
240
+ ## Installation
241
+
242
+ ```bash
243
+ pip install loomtrace
244
+
245
+ ```
246
+
247
+ Optional extras:
248
+
249
+ ```bash
250
+ pip install loomtrace[s3] # S3 support
251
+ pip install loomtrace[redis] # Redis support
252
+ pip install loomtrace[langchain] # LangChain adapter
253
+ pip install loomtrace[web] # Web UI (Flask)
254
+ pip install loomtrace[all] # all of the above
255
+
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Comparison with Existing Tools
261
+
262
+ > **Honest take**: content‑addressed step caching with automatic invalidation is not a new idea. ZenML and Dagster both already do it, well, in production. Loom is a **smaller, single‑purpose** version for standalone scripts.
263
+
264
+ | Tool | Category | What it does | Where it differs from Loom |
265
+ | --- | --- | --- | --- |
266
+ | **ZenML** | ML pipeline platform | Hashes step code, parameters, and artifacts; caches outputs; invalidates on code changes | Full platform: artifact store, stack config, UI, ML‑lifecycle features. You declare pipelines/steps in its framework. |
267
+ | **Dagster** | Data orchestrator | Op/asset memoization with version‑based cache keys; built‑in lineage and scheduling | Full platform — assets, sensors, a runtime you deploy, not a single importable decorator. |
268
+ | LangSmith / Langfuse / Helicone | LLM observability | Log, trace, and visualize LLM calls after the fact | Doesn’t cache or re‑execute — every re‑run still costs full price and time. |
269
+ | MLflow | Experiment tracking | Tracks metrics, params, and artifacts | Not content‑addressed caching; no automatic partial re‑execution. |
270
+ | DVC | Data/pipeline versioning | Content‑addressed, Git‑like caching for **file‑based** pipelines | Built around files and CLI pipeline stages, not live in‑process Python call graphs. |
271
+ | Bazel / Nix | Build systems | Content‑addressed, incremental builds | Not Python‑ or agent‑aware; infrastructure‑level, not a pip‑installable library. |
272
+ | **Loom** | Single‑purpose library | Same core idea (hash code + args, cache, invalidate on change) **+** node‑level diff, but as one dependency‑free decorator with no platform | Smaller, narrower — for standalone scripts. |
273
+
274
+ **The takeaway**: if you already use ZenML or Dagster, their caching is more mature — use it. Loom exists for the case: *“I have a standalone agent script, I don’t want to adopt an orchestration platform, and I want dependency edges inferred automatically from plain Python.”*
275
+
276
+ ---
277
+
278
+ ## Limitations (Read This)
279
+
280
+ * **Steps should be pure.** Caching assumes output depends only on declared inputs. Hidden side effects (global mutation, reading `time.time()`) won’t be tracked correctly.
281
+ * **Value tagging covers most types, not all.** `str`, `tuple`, `frozenset`, `list`, `dict`, `set`, and any object with `__dict__` are tagged directly. `int`, `float`, `bool` (fixed C layout) fall back to a transparent `TracedBox` wrapper – documented, not a silent failure.
282
+ * **`fork()` re‑invokes your pipeline function** – it does not resume from a checkpoint. Speed comes from cache hits, just like Bazel/DVC.
283
+ * **No distributed cache** – but the `Cache` interface is pluggable; `S3Cache` and `RedisCache` ship today.
284
+ * **Async is supported**, but parallel DAG execution is basic (`gather`). True multi‑branch concurrency is planned.
285
+
286
+ ---
287
+
288
+ ## Contributing & Tests
289
+
290
+ Contributions welcome — see `CONTRIBUTING.md`. Run the test suite with:
291
+
292
+ ```bash
293
+ pip install -e ".[dev,all]"
294
+ pytest tests/
295
+
296
+ ```
297
+
298
+ ---
299
+
300
+ ## License
301
+
302
+ MIT — see `LICENSE`.
@@ -0,0 +1,259 @@
1
+ [![PyPI version](https://badge.fury.io/py/loomcache.svg)](https://pypi.org/project/loomcache/)
2
+ # Loom
3
+
4
+ **The smallest tool that gives any Python script content‑addressed caching, partial re‑execution, and node‑level diffing — without an orchestration platform.**
5
+
6
+ Loom treats every function call in your pipeline the way Bazel treats a build target or Git treats a commit: as a hashed, content‑addressed node in a dependency graph. Change one prompt buried deep in a pipeline, and Loom re‑runs **only** that step and everything downstream of it — not the whole pipeline.
7
+
8
+
9
+ ## Table of Contents
10
+
11
+ - [What Loom Solves](#what-loom-solves)
12
+ - [How It Works](#how-it-works)
13
+ - [Quick Start](#quick-start)
14
+ - [Key Features](#key-features)
15
+ - [Core Caching & Execution](#core-caching--execution)
16
+ - [Remote Cache Backends](#remote-cache-backends-s3--redis)
17
+ - [LangChain / LangGraph Adapter](#langchain--langgraph-adapter)
18
+ - [Web UI](#web-ui)
19
+ - [Async Steps & Concurrency](#async-steps--concurrency)
20
+ - [CLI Reference](#cli-reference)
21
+ - [Installation](#installation)
22
+ - [Comparison with Existing Tools](#comparison-with-existing-tools)
23
+ - [Limitations (Read This)](#limitations-read-this)
24
+ - [Roadmap Status](#roadmap-status)
25
+ - [Contributing & Tests](#contributing--tests)
26
+ - [License](#license)
27
+
28
+ ---
29
+
30
+ ## What Loom Solves
31
+
32
+ If you’re hand‑rolling an agent script — not running it through a pipeline platform — you’ve likely faced these problems:
33
+
34
+ - Tweaking **one** prompt or **one** tool halfway through forces you to re‑run the **entire** pipeline — burning tokens, money, and wall‑clock time.
35
+ - When a pipeline’s output changes between two runs, you have no way to see **exactly where** the two runs diverged — you’re left diffing final text blobs and guessing.
36
+ - Debugging means adding print statements and re‑running the whole pipeline again, and again, and again.
37
+
38
+ **Loom solves this** by:
39
+
40
+ 1. **Hashing** every step’s source code + arguments (and upstream node hashes) → deterministic cache keys.
41
+ 2. **Caching** step outputs on disk (or S3/Redis) — identical calls return instantly.
42
+ 3. **Recording** every run as a list of nodes, so you can `diff` two runs node‑by‑node.
43
+ 4. **Forking** a run with a new input — only the changed step and its downstream steps re‑execute.
44
+
45
+ All of this works with **plain Python functions** – no special pipeline declarations, no extra infrastructure.
46
+
47
+ ---
48
+
49
+ ## How It Works
50
+
51
+ Every `@loom.step` call is hashed from:
52
+
53
+ 1. **The source code of the step function itself** — so editing a prompt template inside the function body invalidates the cache automatically.
54
+ 2. **Its arguments** — either their content hash, or, if an argument is itself the traced output of an upstream step, that step’s node hash. This turns a plain chain of Python function calls into a real hashable dependency graph — without any manual wiring.
55
+
56
+ ```text
57
+ user code Loom
58
+ --------- ----
59
+ @loom.step
60
+ def plan(q): ─────► 1. hash(source(plan) + q)
61
+ ... 2. cache lookup
62
+
63
+ hit ◄──┴──► miss
64
+ │ │
65
+ return cached execute plan(q)
66
+ output cache + record
67
+ │ │
68
+ └─────┬──────┘
69
+
70
+ tagged output (carries node hash)
71
+
72
+ passed into the next @loom.step call
73
+
74
+ hash includes the UPSTREAM node hash
75
+ (so changing `plan` invalidates every-
76
+ thing downstream of it automatically)
77
+
78
+
79
+ ---
80
+
81
+ ## Quick Start
82
+
83
+ ```bash
84
+ pip install loomtrace
85
+
86
+ ```
87
+
88
+ ```python
89
+ import loom
90
+
91
+ @loom.step
92
+ def plan(query: str) -> str:
93
+ return llm.call(f"Plan: {query}")
94
+
95
+ @loom.step
96
+ def execute(plan: str) -> str:
97
+ return tool.run(plan)
98
+
99
+ with loom.Run("research-agent") as run:
100
+ p = plan("find competitors of X")
101
+ result = execute(p)
102
+
103
+ run.save()
104
+
105
+ ```
106
+
107
+ Run that pipeline again unchanged — every step is served from cache in milliseconds. Change `plan`’s prompt or the input — only `plan` and its downstream steps re‑run.
108
+
109
+ ---
110
+
111
+ ## Key Features
112
+
113
+ ### Core Caching & Execution
114
+
115
+ * `@loom.step` – caches any function.
116
+ * `loom.Run(name)` – context manager; records every step call.
117
+ * `run.save()` / `Run.load(path)` – persist/restore runs as JSON.
118
+ * `run.fork(pipeline_fn, **kwargs)` – re‑run with new inputs; unchanged steps are cached.
119
+ * `loom.diff_runs(a, b)` / `loom.first_divergence(a, b)` – node‑by‑node diff.
120
+ * `loom.DiskCache(root=...)` – default local cache; subclass `loom.Cache` for other backends.
121
+
122
+ ### Remote Cache Backends (S3 / Redis)
123
+
124
+ ```python
125
+ from loom import S3Cache, RedisCache
126
+
127
+ # S3
128
+ cache = S3Cache(bucket="my-bucket", prefix="loom/")
129
+
130
+ # Redis
131
+ cache = RedisCache(url="redis://localhost:6379/0", key_prefix="loom:")
132
+
133
+ with loom.Run("pipeline", cache=cache) as run:
134
+ ...
135
+
136
+ ```
137
+
138
+ ### LangChain / LangGraph Adapter
139
+
140
+ ```python
141
+ from loom.langchain import wrap_runnable
142
+ from langchain.chains import LLMChain
143
+
144
+ chain = LLMChain(...)
145
+ cached_chain = wrap_runnable(chain, name="my_chain")
146
+
147
+ with loom.Run("lc_run") as run:
148
+ result = cached_chain.invoke({"input": "Hello"})
149
+
150
+ ```
151
+
152
+ ### Web UI
153
+
154
+ ```bash
155
+ loom web --runs-dir .loom_runs --port 5000
156
+
157
+ ```
158
+
159
+ Then open `http://localhost:5000` to browse runs, inspect nodes, and diff runs visually.
160
+
161
+ ### Async Steps & Concurrency
162
+
163
+ ```python
164
+ import asyncio
165
+ import loom
166
+
167
+ @loom.async_step
168
+ async def fetch_data(query: str) -> str:
169
+ await asyncio.sleep(0.1)
170
+ return f"Data for {query}"
171
+
172
+ async def main():
173
+ async with loom.AsyncRun("async_demo") as run:
174
+ results = await loom.gather(
175
+ fetch_data("A"),
176
+ fetch_data("B")
177
+ )
178
+ run.save()
179
+
180
+ asyncio.run(main())
181
+
182
+ ```
183
+
184
+ ---
185
+
186
+ ## CLI Reference
187
+
188
+ | Command | Description |
189
+ | --- | --- |
190
+ | `loom show <run.json>` | List all nodes in a run |
191
+ | `loom diff <a.json> <b.json>` | Node‑by‑node diff |
192
+ | `loom stats <run.json>` | Cache hit rate and timing |
193
+ | `loom web` | Launch the web UI |
194
+
195
+ ---
196
+
197
+ ## Installation
198
+
199
+ ```bash
200
+ pip install loomtrace
201
+
202
+ ```
203
+
204
+ Optional extras:
205
+
206
+ ```bash
207
+ pip install loomtrace[s3] # S3 support
208
+ pip install loomtrace[redis] # Redis support
209
+ pip install loomtrace[langchain] # LangChain adapter
210
+ pip install loomtrace[web] # Web UI (Flask)
211
+ pip install loomtrace[all] # all of the above
212
+
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Comparison with Existing Tools
218
+
219
+ > **Honest take**: content‑addressed step caching with automatic invalidation is not a new idea. ZenML and Dagster both already do it, well, in production. Loom is a **smaller, single‑purpose** version for standalone scripts.
220
+
221
+ | Tool | Category | What it does | Where it differs from Loom |
222
+ | --- | --- | --- | --- |
223
+ | **ZenML** | ML pipeline platform | Hashes step code, parameters, and artifacts; caches outputs; invalidates on code changes | Full platform: artifact store, stack config, UI, ML‑lifecycle features. You declare pipelines/steps in its framework. |
224
+ | **Dagster** | Data orchestrator | Op/asset memoization with version‑based cache keys; built‑in lineage and scheduling | Full platform — assets, sensors, a runtime you deploy, not a single importable decorator. |
225
+ | LangSmith / Langfuse / Helicone | LLM observability | Log, trace, and visualize LLM calls after the fact | Doesn’t cache or re‑execute — every re‑run still costs full price and time. |
226
+ | MLflow | Experiment tracking | Tracks metrics, params, and artifacts | Not content‑addressed caching; no automatic partial re‑execution. |
227
+ | DVC | Data/pipeline versioning | Content‑addressed, Git‑like caching for **file‑based** pipelines | Built around files and CLI pipeline stages, not live in‑process Python call graphs. |
228
+ | Bazel / Nix | Build systems | Content‑addressed, incremental builds | Not Python‑ or agent‑aware; infrastructure‑level, not a pip‑installable library. |
229
+ | **Loom** | Single‑purpose library | Same core idea (hash code + args, cache, invalidate on change) **+** node‑level diff, but as one dependency‑free decorator with no platform | Smaller, narrower — for standalone scripts. |
230
+
231
+ **The takeaway**: if you already use ZenML or Dagster, their caching is more mature — use it. Loom exists for the case: *“I have a standalone agent script, I don’t want to adopt an orchestration platform, and I want dependency edges inferred automatically from plain Python.”*
232
+
233
+ ---
234
+
235
+ ## Limitations (Read This)
236
+
237
+ * **Steps should be pure.** Caching assumes output depends only on declared inputs. Hidden side effects (global mutation, reading `time.time()`) won’t be tracked correctly.
238
+ * **Value tagging covers most types, not all.** `str`, `tuple`, `frozenset`, `list`, `dict`, `set`, and any object with `__dict__` are tagged directly. `int`, `float`, `bool` (fixed C layout) fall back to a transparent `TracedBox` wrapper – documented, not a silent failure.
239
+ * **`fork()` re‑invokes your pipeline function** – it does not resume from a checkpoint. Speed comes from cache hits, just like Bazel/DVC.
240
+ * **No distributed cache** – but the `Cache` interface is pluggable; `S3Cache` and `RedisCache` ship today.
241
+ * **Async is supported**, but parallel DAG execution is basic (`gather`). True multi‑branch concurrency is planned.
242
+
243
+ ---
244
+
245
+ ## Contributing & Tests
246
+
247
+ Contributions welcome — see `CONTRIBUTING.md`. Run the test suite with:
248
+
249
+ ```bash
250
+ pip install -e ".[dev,all]"
251
+ pytest tests/
252
+
253
+ ```
254
+
255
+ ---
256
+
257
+ ## License
258
+
259
+ MIT — see `LICENSE`.