Skip to content

Commit d00a9bf

Browse files
committed
feat: rebuild runtime around durable workflow engine
1 parent 29bb2d6 commit d00a9bf

54 files changed

Lines changed: 3724 additions & 7329 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 25 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,38 @@
11
# Duron
22

3-
[![CI](https://github.com/brian14708/duron/actions/workflows/ci.yaml/badge.svg)](https://github.com/brian14708/duron/actions/workflows/ci.yaml)
4-
[![PyPI - Version](https://img.shields.io/pypi/v/duron)](https://pypi.org/project/duron)
5-
[![Python Versions](https://img.shields.io/pypi/pyversions/duron)](https://pypi.org/project/duron)
6-
[![License](https://img.shields.io/github/license/brian14708/duron.svg)](https://github.com/brian14708/duron/blob/main/LICENSE)
7-
8-
**Durable workflows for modern Python.** Build resilient async applications with native support for streaming and interruption.
9-
10-
- 💬 **Interactive workflows** — AI agents, chatbots, and human-in-the-loop automation with bidirectional streaming
11-
-**Crash recovery** — Deterministic replay from append-only logs means workflows survive restarts
12-
- 🎯 **Graceful interruption** — Cancel or redirect operations mid-execution with signals
13-
- 🔌 **Zero dependencies** — Pure Python built on asyncio, fully typed
14-
- 🧩 **Pluggable storage** — Bring your own database or filesystem backend
15-
16-
Duron replays completed operations from their persisted log position; it does not
17-
provide content-based memoization. External effects have at-least-once semantics:
18-
use idempotency keys or transactions for side effects that must not be duplicated.
19-
20-
## Install
21-
22-
Duron requires **Python 3.10+**.
23-
24-
```bash
25-
uv pip install duron
26-
```
27-
28-
## Quickstart
3+
Duron is a typed durable-workflow runtime for Python 3.10+.
294

305
```python
31-
# /// script
32-
# dependencies = ["duron"]
33-
# ///
34-
35-
import asyncio
36-
from pathlib import Path
37-
from typing import Optional, TypedDict
386
import duron
39-
from duron.contrib.storage import FileLogStorage
40-
41-
42-
class Event(TypedDict):
43-
"""
44-
Event type for communicating workflow progress and approvals.
45-
46-
Fields:
47-
message: Log or status message for the user.
48-
approval_id: Durable future ID to request approval (None for normal logs).
49-
"""
50-
51-
message: str
52-
approval_id: Optional[str]
53-
54-
55-
# -----------------------
56-
# Effect definitions
57-
# -----------------------
58-
59-
60-
@duron.effect
61-
async def check_fraud(amount: float, recipient: str) -> float:
62-
"""Simulate a risk engine returning a fraud probability."""
63-
print("Executing risk check...")
64-
await asyncio.sleep(0.5)
65-
return 0.85
7+
from duron.storage import MemoryStorage
668

9+
events = duron.Output[str]("events")
6710

6811
@duron.effect
69-
async def execute_transfer(amount: float, recipient: str) -> str:
70-
"""Simulate a real transfer execution."""
71-
print("Executing transfer...")
72-
await asyncio.sleep(1)
73-
return f"Transferred ${amount} to {recipient}"
74-
75-
76-
# -----------------------
77-
# Durable workflow
78-
# -----------------------
79-
80-
81-
@duron.durable
82-
async def transfer_workflow(
83-
ctx: duron.Context,
84-
amount: float,
85-
recipient: str,
86-
events: duron.StreamWriter[Event] = duron.Provided,
87-
) -> str:
88-
"""
89-
Durable workflow to execute a transfer with fraud detection
90-
and optional manager approval.
91-
"""
92-
async with events:
93-
# Log start of transfer
94-
await events.send({
95-
"message": f"Checking transfer: ${amount}{recipient}",
96-
"approval_id": None,
97-
})
98-
99-
# Step 1: Fraud check
100-
risk = await ctx.run(check_fraud, amount, recipient)
101-
102-
# Step 2: Approval required if high risk
103-
if risk > 0.8:
104-
approval_id, approval = await ctx.create_future(bool)
105-
await events.send({
106-
"message": "⚠️ High risk - approval required",
107-
"approval_id": approval_id,
108-
})
109-
110-
if not await approval:
111-
await events.send({
112-
"message": "❌ Transfer rejected by manager",
113-
"approval_id": None,
114-
})
115-
return "Transfer rejected"
116-
117-
# Step 3: Execute transfer
118-
result = await ctx.run(execute_transfer, amount, recipient)
119-
await events.send({"message": f"{result}", "approval_id": None})
120-
return result
121-
122-
123-
# -----------------------
124-
# Host process
125-
# -----------------------
126-
127-
128-
async def main():
129-
"""
130-
Run the workflow locally with file-based state storage.
131-
"""
132-
async with duron.Session(FileLogStorage(Path("transfer.jsonl"))) as session:
133-
task = await session.start(transfer_workflow, 10000.0, "suspicious-account")
134-
stream = await task.open_stream("events", "r")
135-
136-
async def handle_events():
137-
async for event in stream:
138-
# Always print message
139-
print(event["message"])
140-
141-
# If approval_id is present, prompt for manager decision
142-
# If the future is not pending, it means it was already resolved (e.g., workflow resumed)
143-
if event["approval_id"] and task.is_future_pending(
144-
event["approval_id"]
145-
):
146-
decision = await asyncio.to_thread(input, "Approve? (y/n): ")
147-
await task.complete_future(
148-
event["approval_id"], result=(decision.lower() == "y")
149-
)
12+
async def fetch_user(user_id: int) -> str:
13+
return f"user-{user_id}"
14+
15+
@duron.workflow
16+
async def greet(ctx: duron.WorkflowContext, user_id: int) -> str:
17+
name = await ctx.call(fetch_user, user_id)
18+
await ctx.emit(events, name)
19+
return f"Hello, {name}"
20+
21+
async with duron.Runtime(MemoryStorage()) as runtime:
22+
run = await runtime.start(greet, id="greeting-42", user_id=42)
23+
assert await run.result() == "Hello, user-42"
24+
```
15025

151-
await asyncio.gather(task.result(), handle_events())
26+
Workflows are deterministic orchestration functions. External work is declared
27+
with `@duron.effect`; durable host interactions use typed module-level
28+
`Input`, `Output`, `Signal`, and `Request` declarations. A `Runtime` manages
29+
many independently identified `Run` objects.
15230

31+
## Install
15332

154-
if __name__ == "__main__":
155-
asyncio.run(main())
33+
```bash
34+
uv pip install duron
15635
```
15736

158-
Duron also provides `MemoryLogStorage` for tests and `SQLiteLogManager` for
159-
multiple named workflow logs in one SQLite database. Writable sessions hold an
160-
opaque fencing lease; readonly sessions may verify completed histories but cannot
161-
start or resume live work.
162-
163-
## Next steps
164-
165-
- Read the [getting started guide](https://brian14708.github.io/duron/getting-started/)
166-
- Explore a more advanced example with streams and signals: [examples/agent.py](https://github.com/brian14708/duron/blob/main/examples/agent.py)
37+
See the [getting started guide](https://brian14708.github.io/duron/getting-started/)
38+
and the API reference for storage, codecs, tracing, and testing.

0 commit comments

Comments
 (0)