A tiny-armed T-Rex that hunts AI-engineering jobs while you sleep – and brings them back for you to judge.
Local-first, single-user autonomous job-hunting agent on a "terrarium" model: a background daemon hunts roles on a schedule, survives the hazards of a mock job-board gym, and drops promising postings into a pen for a human verdict. State is event-sourced – a monotonic SQLite log is the single source of truth – and the frontend is a retro-game projection of that log: the agent's actual trajectory replayed as a creature moving through its world.
Status: build-in-public, early. Persistence (
P1), the typed event model + validation boundary (P2.1), the hand-rolled agent loop +@rex_toolharness (P2.2), the concurrent hunt scheduler (P2.3), and durable pause & HITL — the prey pen + Feast/Release/Amber verdict machine (P4) — are done and gate-tested, all driven by a no-spend stub brain until the realbrain()lands. The brain socket (P5) is next. Slice IDs, order, and gates live in the ADR.
Four of seven invariants; the rest are in the ADR.
- Write-ahead log. Every event commits to SQLite before anyone sees it. The log is truth; the live stream is only a notification – a missed message is replayed from the log. Durability lands before the LLM: a crash at step 9 of a 10-step hunt must never re-pay for steps 1–8.
- Projection, never authority. Everything the UI shows is derived from the log. The live view and the ghost replay of a finished hunt are the same renderer reading the same events at different cursors.
- Validate at the boundary. Raw bytes (scraped HTML, HTTP responses, LLM output) cross exactly one Pydantic validation line on entry; only typed objects exist inside. LLM output is untrusted input.
- Tiny Arms. Rex structurally cannot apply, send, or submit – no such tool is registered. A human "Feast" verdict is a database state transition, not a confirmation dialog.
One data path, end to end. A hunt writes events; everything else is a reader.
hunt task browser
(agent loop) (retro-game UI)
| ^
| 1. append event | 4. SSE stream
v | (Last-Event-ID resume)
+-----------+ 2. after commit +---------------+ |
| SQLite | ------------------> | broadcast hub | --+
| WAL log | 3. notify | (per-viewer |
| (truth) | | async queues)|
+-----------+ +---------------+
^
| many readers, one writer (WAL); a ghost replay
| reads the same log at a different cursor
Downstream is a firehose (SSE, Last-Event-ID resume); upstream is the occasional verdict
(plain HTTP POST). The hub may drop messages – correctness lives in the log, not the stream.
- Python 3.14+ / asyncio – one process, one event loop, no external services.
- FastAPI + uvicorn – lifespan launches the background daemon; SSE + POST endpoints.
- SQLite + aiosqlite, WAL mode – the trajectory store: one file, one writer, many readers.
- Pydantic v2 – events are a discriminated union, validated at the network edge.
- Hand-rolled agent loop – no LangChain, no LangGraph. The ~150-line loop, the tool contract, and the trajectory store are the point of this project; a framework would hide exactly the control flow and durable-state machinery it exists to build.
- Tooling:
ruff,pyright(strict),pytest– pinned inpyproject.toml.
Status projection of the ADR build sequence – the
ADR owns the order and gates; this table tracks only status. Slices are named by slice-ID
(P<n>, pillar-keyed), never a build-order number.
| Slice | What | Pillar | Status |
|---|---|---|---|
| Baseline | Prototype daemon – FastAPI lifespan + loop + SSE + in-memory list | – | Done |
P1 |
Persistence – in-memory list → SQLite WAL event log (runs, trajectory_events) |
1 | Done |
P2.1 |
Typed events – discriminated-union model + validation boundary (events.py) |
2 | Done |
P2.2 |
Loop & tool harness – @rex_tool registry, validate → execute → append, error taxonomy |
2 | Done |
P2.3 |
Hunt scheduler – bounded concurrent-hunt task group + per-territory deadlines, wired into the daemon lifespan | 2 | Done |
P4 |
Durable pause & HITL – prey pen + Feast / Release / Amber verdict machine (verdicts.py) |
4 | Done |
P5 |
Brain socket – provider-agnostic LLM, native tool calling, thinking-token relay | 5 | In progress – Units 1–2c (brain-in-loop, first paid calls) + Unit 3 (streaming ThinkingDelta relay + signed-block replay, one gated thinking-on live hunt) done; extraction/strict + live-adapter daemon wiring remain |
P3 |
Streaming hub – per-viewer broadcast queues, snapshot + catch-up + Last-Event-ID resume |
3 | Done – prototype poll retired; /events streams from the real in-process hub |
Gates are test-first and defined in the ADR. P1's gate (tests/test_stage1_gate.py): a real
hunt subprocess is kill -9ed mid-append; every confirmed event must survive the restart and the
dangling run must be marked 'crashed' at boot. P2.2's gate (tests/test_stage2_gate.py): a
tool that raises, one that hangs past its timeout, and a stub brain naming an unknown tool each
produce typed events and a clean run outcome — never an unhandled exception escaping the loop.
P2.3's gate (tests/test_invariant7.py + tests/test_scheduler.py): the same three failures
hold under a bounded group of N concurrent hunts, each its own writer, with the per-run seq
cursor proven collision-free and gapless by a one-clause WHERE run_id = ? mutant. P4's gate
(tests/test_stage4_gate.py): boot the daemon through the real lifespan, pen a posting, drain,
and boot again over the same file — the prey survives the restart, verdicts still apply, a
double-FEAST and a replay-across-restart are no-ops (one event, one job ever), and the boot
crash-sweep marks a dangling run 'crashed' while leaving the committed prey untouched.
Requires uv – it manages the Python 3.14 toolchain and the virtualenv.
git clone https://github.com/dariero/RexHunter.git
cd RexHunter
uv sync # creates .venv, installs runtime + dev tools from uv.lock
uv run uvicorn --app-dir src rexhunter.server:app --reload # daemon → http://127.0.0.1:8000
# quality gates
uv run ruff check . && uv run ruff format --check .
uv run pyright
uv run pytest # incl. P1's kill -9 crash-durability gaterexhunter-adr.md – the complete design record: five pillars, seven
invariants, and the rejection record for every alternative considered (Postgres, Kafka,
LangGraph, Temporal, and more).