Pyrula
Every step, model call and tool call is journaled, so a run resumes after a crash, reruns with a change, and replays as a test.
pip install "pyrula[agents]"pyrula initpyrula dev app.pyThe scaffold talks to a scripted model, so it needs no key, broker or account. Until release, install is a source build (Install).
Run, what-if, case, CI
Section titled “Run, what-if, case, CI”| On your machine | With Pyrula Cloud | |
|---|---|---|
| Run | pyrula dev journals every step, model and tool call. A completed step is never re-run after a crash. | Every worker’s runs in one timeline (Connect). |
| What-if | pyrula case run --prompt-file or --model: model calls run live, recorded tool results answer the rest. | Fork a finished run and change its input, a step result, the model or the prompt. Writes denied, budget on (What-if runs). |
| Case | pyrula dev keeps the newest finished run in .pyrula/last/. | Save a run or a what-if as a case, stored in your fixture store (Saved cases). |
| CI | pytest --pyrula-cases cases: no model call, no network. | pyrula ci run or the replay-check GitHub Action (Replay checks in CI). |
Record once, iterate free walks the left column.
A safe agent
Section titled “A safe agent”from pyrula.agents import AgentContext, AgentLimits, ToolContext, agent, tool
@tool(read_only=True)async def lookup_order(ctx: ToolContext, order_id: str) -> dict: return {"status": "delivered", "total_cents": 25_000}
@tool(requires_approval=lambda args: args["cents"] > 10_000, approval_timeout=3600)async def refund(ctx: ToolContext, order_id: str, cents: int) -> str: return f"refunded {cents} cents on {order_id}"
@agent(limits=AgentLimits(max_cost_usd=2.50, max_total_tokens=200_000))async def support(ctx: AgentContext, message: str) -> str: return await ctx.llm.run([{"role": "user", "content": message}], tools=[lookup_order, refund])- A refund over $100 parks the run until a person answers from the dashboard or the resume
endpoint; an hour with no answer denies it (Approval).
pyrula dev(embedded mode) refusesapproval_timeout, so run a worker for this agent. Pydantic AI tools get no approval.examples/tool_approval.pyruns an approval and a rejection. - The caps are checked before each live model call, so a run can overshoot by one in-flight call per concurrent caller (Spend caps).
pyrula runs terminate <run_id>ends a run with no worker (Cancel and terminate).- In a what-if or case run,
refundis a write and is refused unless recorded or allowed.
The engine
Section titled “The engine”
Build
Agents
@agent and @tool on the durable engine. Anthropic, OpenAI and LiteLLM clients, MCP, an HTTP surface.
Durable
Workflows
Steps, timers, signals and children, replayed from an append-only journal on SQLite or Valkey.
Stream
Kafka
A pure-Rust client, no librdkafka. Agent output and its offset commit can share one transaction.
Core
Primitives
Rust-backed Either, Option, Try, immutable IList/IMap, serde.
Coming from Pydantic AI, LangGraph, Celery, Temporal or FastAPI? Each page maps that tool’s concepts to Pyrula’s. For coding agents: llms.txt.