nodus-retry
Retry policies, backoff execution, and idempotency gates for AI execution systems.
Named retry policies for each execution type, sync and async execution with
exponential backoff, non-retryable error classification, and an EXACTLY_ONCE
idempotency gate via EffectStore. No required external dependencies — pure stdlib.
Note:
nodus-retryis a required dependency ofnodus-lang>=4.1.0. TheEffectStoreandInMemoryEffectStoreare also available fromnodus-langscripts viastd:effects.
Status: v0.1.0 — published on PyPI.
Install
pip install nodus-retry
What it provides
| Component | Purpose |
|---|---|
RetryPolicy |
Immutable retry configuration (max_attempts, backoff, exponential, guarantee) |
| Named policies | FLOW_NODE_DEFAULT, AGENT_LOW_MEDIUM, AGENT_HIGH_RISK, ASYNC_JOB_DEFAULT, NODUS_SCHEDULED_DEFAULT, NO_RETRY |
resolve_retry_policy |
Look up policy by execution_type + risk_level string |
execute_with_retry |
Run a sync callable under a RetryPolicy |
execute_with_retry_async |
Run an async callable under a RetryPolicy |
is_retryable_error |
Classify error strings as retryable or not |
InMemoryEffectStore |
Thread-safe EXACTLY_ONCE idempotency gate (default; per-process) |
SqliteEffectStore |
Durable, restart-safe idempotency gate (stdlib sqlite3) |
compute_action_id |
SHA-256 hash of (action_type, payload, scope) |
RetryPolicy
from nodus_retry import RetryPolicy
policy = RetryPolicy(
max_attempts=3,
backoff_ms=200,
exponential_backoff=True, # 200ms, 400ms, 800ms
high_risk_immediate_fail=False,
execution_guarantee="AT_LEAST_ONCE", # or "EXACTLY_ONCE"
)
Named policies
from nodus_retry import (
FLOW_NODE_DEFAULT, # 3 attempts, 500ms exponential, AT_LEAST_ONCE
AGENT_LOW_MEDIUM, # 3 attempts, 200ms exponential, AT_LEAST_ONCE
AGENT_HIGH_RISK, # 1 attempt, high_risk_immediate_fail=True
ASYNC_JOB_DEFAULT, # 5 attempts, 1000ms exponential
NODUS_SCHEDULED_DEFAULT, # 3 attempts, 300ms exponential
NO_RETRY, # max_attempts=1
)
Resolve by name
from nodus_retry import resolve_retry_policy
policy = resolve_retry_policy("FLOW_NODE_DEFAULT")
policy = resolve_retry_policy("AGENT_LOW_MEDIUM")
Execution
from nodus_retry import execute_with_retry, execute_with_retry_async, FLOW_NODE_DEFAULT
# Sync
result = execute_with_retry(
lambda: call_external_api(),
policy=FLOW_NODE_DEFAULT,
)
# Async
result = await execute_with_retry_async(
my_async_fn,
policy=AGENT_LOW_MEDIUM,
)
Retries on any exception unless is_retryable_error classifies the error
string as non-retryable (e.g. auth errors, validation errors).
Error classification
from nodus_retry import is_retryable_error
is_retryable_error("connection timeout") # True
is_retryable_error("rate limit exceeded") # True
is_retryable_error("unauthorized") # False
is_retryable_error("invalid input") # False
EffectStore — EXACTLY_ONCE idempotency
from nodus_retry import InMemoryEffectStore, compute_action_id
store = InMemoryEffectStore()
# Compute a deterministic action ID
action_id = compute_action_id("memory.write", {"key": "k", "value": "v"}, scope="run-001")
# Check before executing
already_done, cached = store.resolve(action_id)
if already_done:
return cached # idempotent return
store.pending(action_id, input_hash="h")
try:
result = do_the_work()
store.complete(action_id, "success", result)
return result
except Exception:
store.complete(action_id, "failed", None)
raise
EffectStore is a @runtime_checkable Protocol — any class with resolve,
pending, and complete methods satisfies it.
Durable EXACTLY_ONCE across restarts
InMemoryEffectStore is the default and is per-process — a crash loses its
records. For @exactly_once that survives process restarts, use the drop-in
SqliteEffectStore (stdlib sqlite3, still zero third-party dependencies):
from nodus_retry import SqliteEffectStore
rt.set_effect_store(SqliteEffectStore("effects.db"))
It commits on every write (WAL journal), so a completed effect is durable before
complete() returns; a fresh process pointed at the same file resolves prior
effects idempotently. Pass ":memory:" for a non-durable in-process store. This
does not change the default — durability stays the host's opt-in choice.
Design
- No required dependencies. Pure stdlib (
hashlib,json,sqlite3,threading,asyncio,dataclasses). nodus-lang>=4.1.0requires this package.InMemoryEffectStoreis wired into the VM asself.effect_storeand exposed viastd:effects.
Development
pip install -e ".[dev]"
pytest tests/ -q
License
MIT — see LICENSE.
Metadata
Release files for nodus-retry 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nodus_retry-0.2.0.tar.gz | 13.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nodus_retry-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.0 kB
Release files / nodus_retry-0.2.0.tar.gz
| Download URL | nodus_retry-0.2.0.tar.gz |
|---|---|
| Size | 13.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ba067d8cdbb05d6b27c012961d95b5f02b6053406478731499f46a494c029738
|
|
BLAKE2b-256 checksum How to use checksums |
a547f325b6bf54ba118377446c5c3a24a457b0f97a396b9d8366077e6b53beee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / nodus_retry-0.2.0-py3-none-any.whl
| Download URL | nodus_retry-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4d91cfa61762be11ce09c99acd177ce2472bab055ab9c081c9942665bc185f90
|
|
BLAKE2b-256 checksum How to use checksums |
a17635782d197e786aee84cd6898f363d1aed36d0cb12367d7f4f1494810dbd8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|