Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Workers

In separate mode the API submits and streams, and PyrulaWorker claims turns from the shared store, runs them, and handles timers, signals, orphan recovery and poison turns.

from pyrula.agents import PyrulaWorker
from pyrula.agents.llm import OpenAILLM
worker = PyrulaWorker(
agents=[support], # or omit for the registry
store="redis://localhost:6379",
llm=OpenAILLM(model="gpt-6-luna"),
concurrency=50,
)
worker.run()

store is a Valkey or Redis URL or a store instance, and must match the API’s. With no agents, it serves every imported @agent (ValueError if none). If PYRULA_CLOUD_API_KEY is set the worker attaches to Cloud; cloud=False turns that off. run() blocks; run_async() is awaitable. worker.stop() stops claiming, lets in-flight turns finish within a 30-second grace, cancels the rest so another worker can recover them, stops the cloud connector, then returns. Call it from a signal handler.

from pyrula.agents import WorkerOptions, PoisonOptions
WorkerOptions(
max_concurrent_turns=50,
orphan_scan_interval=60.0,
timer_poll_interval_s=5.0,
schedule_poll_interval_s=30.0,
poison=PoisonOptions(max_retries=3, retry_ttl_s=3600.0),
)

A turn that keeps crashing the worker is retried up to PoisonOptions.max_retries (3), counted for retry_ttl_s (1 h), then quarantined: run:error with a poison reason and RunStatus.quarantined.

A turn whose code blocks the event loop (a sync SDK called from an async tool, say) past max(3 × lock_ttl, timeout) stops renewing its lock and moves to another claim. Each of those counts toward the same budget, for the turn whose code was blocking only, not for other turns on the same worker. The quarantine has error_type EventLoopBlocked and detail.cause loop_blocked. The agent’s timeout and a tool’s timeout= cannot fire while the loop is blocked.

Another worker claims a dead worker’s turn on its orphan scan and resumes from the first unfinished step. Sleeping and signal-waiting turns aren’t orphans. Queued turns are picked up right away, and a clean stop() releases its shards immediately (Scaling across shards; replay and recovery).

store="redis://..." builds a default ValkeyStore; pass ValkeyStore(shard_count=...) to configure sharding. A cluster is detected from INFO; cluster=True overrides that for one behind a proxy that hides it. The agents PostgresStore takes the same shard_count, shard_lease_ttl and cluster for its live tier, plus live_ttl_after_archive (default 300 s).

PyrulaWorker(health_port=8081) (or PYRULA_WORKER_HEALTH_PORT) serves /livez and /readyz; health_host / PYRULA_WORKER_HEALTH_HOST sets the bind address (default 127.0.0.1). /readyz turns 503 once stop() is called. See Health probes and Deploy a worker.

View this page as Markdown