Background tasks

Werbos's background work queue: SQLite-backed task store, asyncio runner that yields to foreground turns, /tasks slash command. The plumbing that lets agents run while you're not looking.

Werbos has a background work queue. Today it runs one stub executor (stats-refresh) — the surface exists for the dogfood-replay agent, inbox janitor, and continuity-of-intent agent that are queued in the roadmap.

The defining property of every background agent here: it yields to foreground turns. When you’re mid-chat, the runner stays out of the way. When the AI turn-lock releases, the runner picks up where it left off. No 5-second stalls when you start a new chat.

The pieces

werbos.tasks.TaskStore

SQLite-backed, optimistic-lock CAS via a version column. Status transition rules: pending → running → done | failed | cancelled. UUIDv7 ids (time-sortable). tenant_id and user_id columns from day 1, defaulting to "default" / "local" in single-user mode — the gatekeep multi-tenant seam is baked in so a future hosted SKU is a default-value change, not a refactor.

werbos.tasks.TaskRunner

asyncio loop that lives in the Starlette lifespan. Each iteration:

  1. Check if the foreground is busy via observe’s foreground-busy callable. If yes, sleep.
  2. Pull the next pending task from the store (FIFO by created_at).
  3. Dispatch to the registered Executor for that task’s kind.
  4. Capture exception type + message into the row on failure; never crashes the loop.

Executors

A registered handler for one task kind. Today there’s one:

  • stats-refresh — rebuilds werbos stats-style aggregates off-thread when a chat archive lands. Lightweight enough to prove the runner end-to-end.

The dogfood-replay executor is the next slot — it’ll watch the chat archive directory, pick recently-bad-labeled turns, and replay them through the latest model / persona / LoRA, writing per-turn rows to ~/werbos/regressions.jsonl.

HTTP surface

GET    /api/tasks               # list (paginated; observe-shaped)
POST   /api/tasks               # create — body: {kind, payload}
GET    /api/tasks/{id}          # one task with status + result

Every error response uses observe’s {error, detail} envelope and ISO-8601 UTC Z-suffix timestamps so a watchtower runtime polling this endpoint sees a consistent shape.

Tenant scoping is server-side: the request’s resolved tenant_id filters reads and stamps writes. Single-user mode is transparent — every request resolves to "default" / "local".

/tasks slash command

Inside a chat, you can enqueue and inspect tasks without leaving the conversation:

/tasks                          # list your pending + recent tasks
/tasks <kind>                   # enqueue a task of that kind
/cancel <id>                    # mark a pending task cancelled

The slash dispatch routes through the chat surface to the same HTTP endpoints. The chat turn that enqueued the task gets the task id back inline; the runner picks it up next idle moment; future chat turns can ask for status by id.

Foreground-busy

Werbos’s foreground state is exposed two ways for orchestration consumers:

In-process

from werbos.observe import make_foreground_busy_callable

is_busy = make_foreground_busy_callable(session_registry)
if is_busy():
    # AI turn in flight; defer the agent dispatch
    ...

The callable matches observe SPEC v1 § 4 exactly: Callable[[], bool], synchronous, side-effect-free, returns in microseconds.

HTTP

GET /api/observe/foreground-busy
{
  "protocol": "OBSERVE-PROTO-v1",
  "foregroundBusy": false,
  "asOf": "2026-05-08T07:00:00.000Z"
}

A separate-process watchtower runtime, a cron-driven warden, or a Prometheus shim can poll this without coupling to werbos’s Python surface. Round-trip is <5 ms with no active sessions.

What’s queued

  • Watchtower Dispatch consumer — observe SPEC § 2.4.1 webhook with HMAC verification. Will land at /api/observe/dispatch so triggers fired by a separate-process watchtower can drive werbos agents.
  • Pending-actions endpoint — /api/pending with /approve / /deny slash commands. Lets background agents propose work that the user must explicitly green-light before it runs.
  • Dogfood-replay agent. The first real (non-stub) executor. Watches ~/werbos/chats/, picks recently-bad-labeled turns, re-runs them through the latest model, writes regression rows.
  • Inbox janitor / dispatcher. Watches ~/werbos/inbox/, sorts easy stuff locally, escalates hard stuff to Claude for triage.
  • Daily Claude budget — cap on background-agent escalations (e.g. 100 calls / $5/day). Logged usage, “refuse + queue for tomorrow” when capped.