CLI
Installing pyrula-agents puts a pyrula command on your path. It covers the
operational side: running agents locally, running the worker tier, and managing the
store and in-flight turns.
pyrula --help # dev | worker | db | turns | schedules | reprojectpyrula dev accepts file targets such as module.py:agent_name. Worker commands use
Python import refs such as your_module:agent_name; configured LLM or deps objects use
the same import-ref shape, for example your_module:llm.
pyrula dev is the local cockpit. One command, two modes — the input picks which:
pyrula dev # auto-discover app.py/agents.py here, run it, open the UIpyrula dev app.py # find the @agent in the module and run itpyrula dev app.py:support # explicit targetpyrula dev --store valkey://… # attach to a running system, browse every run| Mode | What it does | Needs |
|---|---|---|
| Embedded (a target, or auto-discovered) | runs the agent in-process on an in-memory store so its runs show up in the UI | nothing — zero infra |
Attach (--store …) | browses every agent and workflow run in a shared store | a shared store (Valkey) your workers already use |
The UI is read-only either way: it shows your agents and workflows and their runs. You
start runs the way you always do (your worker, your code, the API); pyrula dev is the
window onto them.
With no --valkey-url, embedded mode uses an in-memory store, so there’s nothing to set
up. If you give a bare module (or run pyrula dev with no arguments), it discovers the
@agent targets for you; with several, it lists them so you can pick one.
pyrula dev app.py:support --llm-ref app:llm --port 8000Setup gradient
Section titled “Setup gradient”Pick the tier you need — each is one command, and your code never changes between them:
- Zero infra (instant):
pyrula dev app.py:agent --open. In-memory store, embedded worker, live UI. Nothing to install or run. - Full multi-process visibility (one container): run Valkey
(
docker run -p 6379:6379 valkey/valkey), point your workers andpyrula dev --store valkey://localhost:6379at it. Now the cockpit sees every run, no matter which process produced it. - Your real deployment:
pyrula dev --store <your valkey>— read-only attach to a staging-like store during local work.
Attach mode (--store)
Section titled “Attach mode (--store)”pyrula dev --store valkey://localhost:6379 opens the UI with no agent loaded and no
worker. It reads from the store your system already writes to and lists every name —
agents and plain @workflows alike — with their runs; open any run for its live timeline.
Because the event renderers are pure functions on the event payload, agent transcripts
(LLM/tool/block) render without importing your agent code. It requires a shared store —
in-memory is per-process and can’t see another process’s runs.
Local dev UI
Section titled “Local dev UI”pyrula dev mounts a small web UI at /ui (on by default). Open
http://127.0.0.1:8000/ui and you see your agents and workflows, each with its runs.
Click a run for a live timeline that streams its events as they happen — tool calls, LLM
steps, and errors each render as a row, with the payload inline.
pyrula dev app.py:support --llm-ref app:llm --open # open the browser on startuppyrula dev app.py:support --llm-ref app:llm --no-ui # API only, no UIThe UI is local-only and unauthenticated by design: it binds to the dev server, holds no state of its own, and is meant for the inner-loop of building an agent. It is not a hosted console.
Mount it into your own app
Section titled “Mount it into your own app”pyrula dev is a convenience wrapper. If you already run your own FastAPI app — or a
separate-worker fleet — you can mount the same UI yourself. It attaches to any FastAPI
app alongside your agent routes:
from pyrula.agents.web.router import AgentRouterfrom pyrula.agents.web.ui_ext import register_agent_renderersfrom pyrula.workflows.web.ui import mount_run_ui
router = AgentRouter(worker=worker) # InlineWorker, or a WorkerProxy for separate workersrouter.attach(app, prefix="/agents") # your existing FastAPI appregister_agent_renderers() # llm/tool/block transcript renderersmount_run_ui(app, prefix="/ui", control=router)The UI reads runs from the shared store, so with a separate-worker deployment it needs a real store (Valkey/Postgres), not the in-memory one. The same no-auth caveat applies — keep it behind localhost or a private network; it is not a drop-in production console (that is Pyrula Cloud).
worker
Section titled “worker”Run the worker tier for separate-mode deployments. Point it at the same store the API uses.
pyrula worker \ --agent app:support \ --valkey-url redis://localhost:6379 \ --llm-ref app:llm \ --deps-ref app:services \ --max-concurrent-turns 50 \ --poison-max-retries 3--deps-ref injects a shared dependencies object. The poison, heartbeat, and
orphan-scan flags map to the same WorkerOptions knobs covered in
Workers.
Valkey self-bootstraps, so it needs no setup. Postgres archival needs its schema
applied once. Use check to validate connectivity before starting workers.
pyrula db check --valkey-url redis://localhost:6379pyrula db migrate --postgres-dsn "$PYRULA_POSTGRES_DSN"Inspect and recover individual turns.
pyrula turns list-quarantined --agent support --limit 50pyrula turns inspect --turn-id <id> --agent supportpyrula turns requeue --turn-id <id> --agent supportpyrula turns gc --older-than 30d --postgres-dsn "$PYRULA_POSTGRES_DSN" --dry-runrequeue takes a turn the worker quarantined as poison and
puts it back in the queue, once you’ve fixed whatever broke it. gc deletes archived
turns past a cutoff; --dry-run counts them first.
Scheduled runs have their own subcommand, covered in Schedules.
reproject
Section titled “reproject”Rebuild the conversation, usage, and cost read tables from the archived event log. Use it to backfill runs archived before you enabled projection, or after editing the price table. Needs Postgres.
pyrula reproject --agent support # one agentpyrula reproject --agent support --run-id <id> # one run (--run-id requires --agent)pyrula reproject # every archived run, every agentProjection itself is opt-in on the store (PostgresStore(..., project=True)) and is
covered in Observability.