mycelium-runtime 1.1.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 (43) hide show
  1. mycelium_runtime-1.1.0/.gitignore +8 -0
  2. mycelium_runtime-1.1.0/PKG-INFO +378 -0
  3. mycelium_runtime-1.1.0/README.md +347 -0
  4. mycelium_runtime-1.1.0/examples/README.md +13 -0
  5. mycelium_runtime-1.1.0/mycelium/__init__.py +121 -0
  6. mycelium_runtime-1.1.0/mycelium/__main__.py +64 -0
  7. mycelium_runtime-1.1.0/mycelium/action_ledger.py +431 -0
  8. mycelium_runtime-1.1.0/mycelium/audit_receipt.py +252 -0
  9. mycelium_runtime-1.1.0/mycelium/cache.py +49 -0
  10. mycelium_runtime-1.1.0/mycelium/config.py +755 -0
  11. mycelium_runtime-1.1.0/mycelium/history_guard.py +228 -0
  12. mycelium_runtime-1.1.0/mycelium/message_validator.py +319 -0
  13. mycelium_runtime-1.1.0/mycelium/protect.py +137 -0
  14. mycelium_runtime-1.1.0/mycelium/schema.py +41 -0
  15. mycelium_runtime-1.1.0/mycelium/session.py +50 -0
  16. mycelium_runtime-1.1.0/mycelium/state_flush.py +246 -0
  17. mycelium_runtime-1.1.0/mycelium/storage/__init__.py +21 -0
  18. mycelium_runtime-1.1.0/mycelium/storage/_helpers.py +45 -0
  19. mycelium_runtime-1.1.0/mycelium/storage/file_lock.py +51 -0
  20. mycelium_runtime-1.1.0/mycelium/storage/json_file.py +59 -0
  21. mycelium_runtime-1.1.0/mycelium/storage/postgres_ledger.py +216 -0
  22. mycelium_runtime-1.1.0/mycelium/storage/redis_ledger.py +168 -0
  23. mycelium_runtime-1.1.0/mycelium/task_ledger.py +432 -0
  24. mycelium_runtime-1.1.0/mycelium/templates/mycelium.minimal.yaml +81 -0
  25. mycelium_runtime-1.1.0/mycelium/templates/mycelium.template.yaml +147 -0
  26. mycelium_runtime-1.1.0/mycelium/tool_boundary.py +351 -0
  27. mycelium_runtime-1.1.0/mycelium/tool_registry.py +54 -0
  28. mycelium_runtime-1.1.0/mycelium/tool_runner.py +122 -0
  29. mycelium_runtime-1.1.0/pyproject.toml +58 -0
  30. mycelium_runtime-1.1.0/tests/test_audit_receipt.py +89 -0
  31. mycelium_runtime-1.1.0/tests/test_cli.py +37 -0
  32. mycelium_runtime-1.1.0/tests/test_config.py +445 -0
  33. mycelium_runtime-1.1.0/tests/test_history_guard.py +77 -0
  34. mycelium_runtime-1.1.0/tests/test_message_validator.py +134 -0
  35. mycelium_runtime-1.1.0/tests/test_protect.py +97 -0
  36. mycelium_runtime-1.1.0/tests/test_protect_sync.py +97 -0
  37. mycelium_runtime-1.1.0/tests/test_session.py +86 -0
  38. mycelium_runtime-1.1.0/tests/test_state_flush.py +100 -0
  39. mycelium_runtime-1.1.0/tests/test_storage_backends.py +158 -0
  40. mycelium_runtime-1.1.0/tests/test_tool_boundary.py +165 -0
  41. mycelium_runtime-1.1.0/tests/test_tool_registry.py +32 -0
  42. mycelium_runtime-1.1.0/tests/test_tool_runner.py +118 -0
  43. mycelium_runtime-1.1.0/uv.lock +574 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ dist/
8
+ build/
@@ -0,0 +1,378 @@
1
+ Metadata-Version: 2.4
2
+ Name: mycelium-runtime
3
+ Version: 1.1.0
4
+ Summary: Mycelium runtime — failure prevention for AI agents (AF-006, AF-004, AF-002)
5
+ Project-URL: Homepage, https://github.com/mycelium-labs/mycelium
6
+ Project-URL: Repository, https://github.com/mycelium-labs/mycelium
7
+ Project-URL: Issues, https://github.com/mycelium-labs/mycelium/issues
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: pydantic<3,>=2
20
+ Requires-Dist: pyyaml>=6
21
+ Provides-Extra: dev
22
+ Requires-Dist: fakeredis; extra == 'dev'
23
+ Requires-Dist: pytest; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio; extra == 'dev'
25
+ Requires-Dist: ruff; extra == 'dev'
26
+ Provides-Extra: postgres
27
+ Requires-Dist: psycopg[binary]>=3; extra == 'postgres'
28
+ Provides-Extra: redis
29
+ Requires-Dist: redis>=5; extra == 'redis'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Mycelium runtime
33
+
34
+ Runtime failure prevention for AI agents.
35
+
36
+ **PyPI:** `pip install mycelium-runtime` — **import:** `from mycelium import ...`
37
+
38
+ This directory is the **publishable package**. Only `mycelium/` ships on PyPI; everything else here is for development and docs.
39
+
40
+ | Path | In PyPI wheel? |
41
+ |------|----------------|
42
+ | `mycelium/` | Yes (code + `templates/*.yaml`) |
43
+ | `mycelium/templates/` | Yes — use `mycelium init` to write into your project |
44
+ | `tests/` | No — run with `pytest tests/` |
45
+ | `examples/` | No — see `mycelium init` |
46
+ | `README.md` | PyPI project page only (not inside wheel) |
47
+ | `pyproject.toml`, `uv.lock` | No |
48
+
49
+ **v1.1** ships three failure modes: context corruption (AF-006), tool boundary enforcement (AF-004), and **action traceability prevention** (AF-002).
50
+
51
+ > **AF-002 is not an observability platform.** The failure mode is called "observability black hole" because agents act without durable records. Mycelium **prevents** that — via idempotency ledgers, state flush on cancel, and signed receipts — not via traces, spans, or dashboards. For post-hoc tracing, use Langfuse, Helicone, or Opik alongside Mycelium.
52
+
53
+ ## Install
54
+
55
+ **Requires Python 3.10+** (3.11+ recommended).
56
+
57
+ ```bash
58
+ pip install mycelium-runtime
59
+ mycelium init # writes ./mycelium.yaml (full annotated template)
60
+ mycelium init --minimal # smaller starter config
61
+ # local dev from repo:
62
+ pip install -e ./sdk
63
+ ```
64
+
65
+ ## Quickstart — AF-006
66
+
67
+ ```python
68
+ from mycelium import protect, Session
69
+
70
+ @protect(entity_param="customer_id", ttl=60)
71
+ async def fetch_customer(customer_id: str) -> dict:
72
+ return await db.get(customer_id)
73
+
74
+ async def handle_request(customer_id: str):
75
+ async with Session():
76
+ return await fetch_customer(customer_id=customer_id)
77
+ ```
78
+
79
+ Sync tools (CrewAI, Smolagents):
80
+
81
+ ```python
82
+ from mycelium import protect_sync, Session
83
+
84
+ @protect_sync(entity_param="customer_id", ttl=60)
85
+ def fetch_customer(customer_id: str) -> dict:
86
+ return db.get(customer_id)
87
+
88
+ with Session():
89
+ customer = fetch_customer(customer_id="c1")
90
+ ```
91
+
92
+ ## What `@protect` / `protect_sync` / `Session` do
93
+
94
+ - `@protect` / `protect_sync` — TTL cache with per-entity keys; auto-refetch when stale; clear on error
95
+ - `Session` — one cache per agent run; use in production to prevent cross-request leakage
96
+
97
+ ## MessageValidator
98
+
99
+ Run before each LLM call to catch broken transcripts:
100
+
101
+ ```python
102
+ from mycelium import MessageValidator
103
+
104
+ messages = MessageValidator().repair(messages) # auto-fix what it can
105
+ # or
106
+ messages = MessageValidator().validate(messages) # raise on first issue
107
+ ```
108
+
109
+ Catches orphan tool results, duplicate tool-call IDs, invalid roles, and related serialization bugs.
110
+
111
+ ## HistoryGuard
112
+
113
+ Run before each LLM call to catch oversized or corrupted history:
114
+
115
+ ```python
116
+ from mycelium import HistoryGuard
117
+
118
+ guard = HistoryGuard(max_tokens=100_000)
119
+ messages = guard.validate(messages)
120
+ guard.check_for_drops(processed_messages) # after framework trimming
121
+ ```
122
+
123
+ Raises on token overflow, message count limits, duplicate turns, and silent message drops.
124
+
125
+ ## Quickstart — AF-004
126
+
127
+ ```python
128
+ from mycelium import bounded, ToolRegistry, ToolRunner
129
+
130
+ FETCH_CUSTOMER_SCHEMA = {
131
+ "customer_id": {"type": "string", "required": True, "pattern": r"^c\d+$"},
132
+ }
133
+
134
+ CUSTOMER_RECORD_SCHEMA = {
135
+ "customer_id": {"type": "string", "required": True},
136
+ "name": {"type": "string", "required": True},
137
+ }
138
+
139
+ registry = ToolRegistry(allowed=["fetch_customer"])
140
+
141
+ @registry.register
142
+ @bounded(
143
+ schema=FETCH_CUSTOMER_SCHEMA,
144
+ output_schema=CUSTOMER_RECORD_SCHEMA,
145
+ allowed_paths=["/workspace/src/"],
146
+ )
147
+ async def fetch_customer(customer_id: str) -> dict:
148
+ return await db.get(customer_id)
149
+
150
+ runner = ToolRunner(registry=registry)
151
+ result = await runner.call(fetch_customer, customer_id="c1")
152
+ ```
153
+
154
+ Sync tools:
155
+
156
+ ```python
157
+ from mycelium import bounded_sync
158
+
159
+ @bounded_sync(schema=FETCH_CUSTOMER_SCHEMA)
160
+ def fetch_customer(customer_id: str) -> dict:
161
+ return db.get(customer_id)
162
+ ```
163
+
164
+ Field spec keys: `type` (`string`, `integer`, `number`, `boolean`), `required`, `pattern`, `min_length`, `max_length`. You pass plain dicts — Mycelium validates internally; no Pydantic imports in your code.
165
+
166
+ ## What `@bounded` / `bounded_sync` do
167
+
168
+ - `@bounded` / `bounded_sync` — validate tool args against your field spec **before** the function runs
169
+ - `output_schema` — validate the return value **after** the function runs; bad results are not propagated
170
+ - `allowed_paths` / `entity_pattern` — user-defined scope gates (path prefixes, entity ID format)
171
+ - On failure, raises `ToolBoundaryError` with `llm_message` for the agent loop — does not retry by itself
172
+
173
+ ## ToolRegistry
174
+
175
+ Run before dispatch to enforce which tools this agent may call:
176
+
177
+ ```python
178
+ from mycelium import ToolRegistry
179
+
180
+ registry = ToolRegistry(allowed=["search_docs", "summarize"])
181
+ registry.validate_call("fetch_customer") # raises ToolBoundaryError
182
+ ```
183
+
184
+ Blocks calls to tools outside the developer-defined allowlist.
185
+
186
+ ## ToolRunner
187
+
188
+ Run around `@bounded` tools when you want automatic retries:
189
+
190
+ ```python
191
+ from mycelium import ToolRunner
192
+
193
+ runner = ToolRunner(registry=registry, max_llm_retries=2, max_tool_retries=3)
194
+
195
+ result, messages = await runner.run_with_llm_retry(
196
+ fetch_customer,
197
+ messages=messages,
198
+ tool_call_id="call_1",
199
+ kwargs={"customer_id": "c1"},
200
+ invoke_llm=llm.ainvoke,
201
+ parse_tool_kwargs=extract_tool_args,
202
+ )
203
+ ```
204
+
205
+ - Input, allowlist, and scope failures → append tool error to messages → LLM retry
206
+ - Output failures → retry the tool up to `max_tool_retries` → then LLM retry
207
+ - Raises `ToolBoundaryExhaustedError` when retries are used up
208
+
209
+ ## Quickstart — AF-002 (action traceability prevention)
210
+
211
+ AF-002 prevents the "observability black hole" failure class: duplicate side effects, lost state on cancel, and actions that can't be audited. This is **not** distributed tracing — see the note at the top of this README.
212
+
213
+ ### Tool-level idempotency
214
+
215
+ ```python
216
+ from mycelium import ledger_sync
217
+
218
+ @ledger_sync()
219
+ def send_payment(amount: float, recipient: str) -> dict:
220
+ return gateway.charge(amount, recipient)
221
+
222
+ # Same logical call executes only once.
223
+ send_payment(amount=100.0, recipient="acct_123", request_id="invoice-42")
224
+ send_payment(amount=100.0, recipient="acct_123", request_id="invoice-42")
225
+ ```
226
+
227
+ Async tools:
228
+
229
+ ```python
230
+ from mycelium import ledger
231
+
232
+ @ledger()
233
+ async def send_payment(amount: float, recipient: str) -> dict:
234
+ return await gateway.charge(amount, recipient)
235
+ ```
236
+
237
+ ## What `@ledger` / `ledger_sync` do
238
+
239
+ - Record every tool invocation in a durable `ActionLedger`
240
+ - Deduplicate retries and redispatches by `request_id` or LLM `tool_call_id`
241
+ - Allow legitimate repeats when the request id differs
242
+ - Persist failed attempts for audit and debugging
243
+
244
+ Storage backends:
245
+
246
+ | Backend | Use case | YAML `storage` |
247
+ |---------|----------|----------------|
248
+ | `memory` | Single process, tests | `memory` (default) |
249
+ | `file` | Local dev, single host (`fcntl` lock) | `file` + `path` |
250
+ | `redis` | Multi-worker, in-flight TTL | `redis` + `url` or `url_env` |
251
+ | `postgres` | Audit/compliance, durable SQL | `postgres` + `dsn` or `dsn_env` |
252
+
253
+ ```python
254
+ from mycelium import ActionLedger, FileLedgerStorage, InMemoryLedgerStorage
255
+ from mycelium import RedisLedgerStorage, PostgresLedgerStorage
256
+
257
+ ledger = ActionLedger(storage=InMemoryLedgerStorage())
258
+ ledger = ActionLedger(storage=FileLedgerStorage("./mycelium-ledger.json"))
259
+ ledger = ActionLedger(storage=RedisLedgerStorage("redis://localhost:6379/0"))
260
+ ledger = ActionLedger(storage=PostgresLedgerStorage("postgresql://localhost/mycelium"))
261
+ ```
262
+
263
+ Optional extras: `pip install 'mycelium-runtime[redis]'` or `pip install 'mycelium-runtime[postgres]'`.
264
+
265
+ ## Quickstart — AF-002 task-level ledger
266
+
267
+ Stop entire tasks from re-running on framework-level retries:
268
+
269
+ ```python
270
+ from mycelium import task_ledger_sync
271
+
272
+ @task_ledger_sync()
273
+ def process_invoice(invoice_id: str) -> dict:
274
+ customer = fetch_customer(customer_id=...)
275
+ payment = send_payment(...)
276
+ return {"invoice_id": invoice_id, "status": "paid"}
277
+
278
+ # Framework retries the task with the same task_id
279
+ process_invoice(invoice_id="inv-42", task_id="invoice-42") # executes
280
+ process_invoice(invoice_id="inv-42", task_id="invoice-42") # returns stored result
281
+ ```
282
+
283
+ Use `id_from` to derive the task id from business keys automatically:
284
+
285
+ ```python
286
+ @task_ledger_sync(id_from=["invoice_id"])
287
+ def process_invoice(invoice_id: str, amount: float) -> dict:
288
+ ...
289
+
290
+ # Both calls map to the same task id because invoice_id is the same.
291
+ process_invoice(invoice_id="inv-42", amount=100.0)
292
+ process_invoice(invoice_id="inv-42", amount=200.0) # returns first result
293
+ ```
294
+
295
+ ### Correction retries
296
+
297
+ If a completed task produced a bad result and the LLM/agent needs to re-attempt it, use a **new task id**. The framework will normally generate fresh tool call ids for the new attempt, so the task re-executes cleanly.
298
+
299
+ ```python
300
+ r1 = process_invoice(invoice_id="inv-42", task_id="invoice-42-attempt-1") # bad result
301
+ r2 = process_invoice(invoice_id="inv-42", task_id="invoice-42-attempt-2") # fresh attempt
302
+ ```
303
+
304
+ ## YAML configuration
305
+
306
+ Separate sections per failure mode. Global AF-002 settings inherit into tools/tasks
307
+ so you do not repeat storage paths on every function.
308
+
309
+ **Minimum integration (3 steps):**
310
+
311
+ ```yaml
312
+ # mycelium.yaml — global sections (configure once)
313
+ action_ledger:
314
+ storage: file
315
+ path: ./mycelium-ledger.json
316
+ tools: [send_payment] # auto-ledger side-effect tools
317
+
318
+ task_ledger:
319
+ storage: file
320
+ path: ./mycelium-task-ledger.json
321
+ tasks: [process_invoice]
322
+
323
+ state_flush:
324
+ storage: file
325
+ path: ./mycelium-state.json
326
+
327
+ audit_receipt:
328
+ agent_id: my-agent
329
+ signing_key_env: MYCELIUM_SIGNING_KEY
330
+ storage: file
331
+ path: ./mycelium-receipts.jsonl
332
+
333
+ # Per-tool: only what differs (schemas, cache, etc.)
334
+ tools:
335
+ fetch_customer:
336
+ protect: {entity_param: customer_id, ttl: 60}
337
+ bounded:
338
+ schema:
339
+ customer_id: {type: string, required: true, pattern: "^c\\d+$"}
340
+
341
+ send_payment:
342
+ bounded:
343
+ schema:
344
+ amount: {type: number, required: true}
345
+ recipient: {type: string, required: true}
346
+
347
+ tasks:
348
+ process_invoice:
349
+ ledger: true
350
+ id_from: [invoice_id]
351
+
352
+ registry:
353
+ auto: true # allowlist = all configured tools
354
+
355
+ history_guard:
356
+ max_tokens: 100000
357
+
358
+ message_validator:
359
+ enabled: true
360
+ ```
361
+
362
+ ```python
363
+ from mycelium import load_config
364
+ import my_tools
365
+
366
+ config = load_config("mycelium.yaml")
367
+ tools = config.instrument(my_tools) # one call wraps tools + tasks
368
+
369
+ with config.run(thread_id):
370
+ messages = config.prepare_messages(messages) # AF-006 + auto state flush
371
+ ...
372
+ ```
373
+
374
+ `ledger: true` inherits from `action_ledger` / `task_ledger`. When `audit_receipt`
375
+ is configured with `auto: true` (default), all ledgered tools/tasks get signed
376
+ receipts automatically.
377
+
378
+ Legacy per-tool style still works — run `mycelium init` for the full annotated template.