Technical deepdive
AgentLedger is a FastAPI + Postgres/pgvector backend, a React SPA, and one 20-tool MCP — all sitting on a single shared service layer. Here's the architecture, the agent-loop mechanics, and the data model that makes it work.
System topology
Humans enter through REST with a JWT; agents enter through the MCP endpoint with a scoped API key. Both converge on the same service layer, which is the only thing that touches the database.
Routers and the MCP server are thin adapters — every tool call and every REST request runs
the same function. There is exactly one path to the database, so an agent's
create_item appears in the web tracker with no
reconciliation, and every capability is available to both people and agents for free.
The differentiator
claim_next reads the best ready candidate then does an optimistic UPDATE guarded on claimed_by — only one caller wins the row, so two agents never claim the same item. A claim carries a lease (claimed_at); heartbeat extends it, and a lease that goes stale past the window is reclaimable, so a crashed agent's in-progress work is picked up by another. agent_id defaults to the API key's name — one key, one agent.
Items declare touchpoints — files, globs, or modules they affect. Two items relate when touchpoints overlap (exact, glob, or same directory), and overlap auto-creates a code link. next_cluster claims the best ready item plus its related ready neighbours in one call, so an agent pulls a whole code-neighborhood and works it in one context instead of thrashing.
Readiness is computed, not flagged: an item is blocked until every item it depends on is done. The backlog sorts ready-first, then by a composite score — status, dependency fan-out (unblocking many ranks higher), request votes rolled onto the linked item, effort, and staleness. claim_next and next_cluster consume the same ranking, so agents always take the highest-leverage ready work.
Items link to a PRD section. decompose_prd parses a spec's sections and creates a tracked task per gap, linked back to the section; prd_coverage returns the per-section rollup — counts by status, percent done, and the sections with no tasks yet. Completing work updates coverage, and the loop asks the spec what's next.
When an item is completed, AgentLedger auto-extracts its lessons into pgvector memory.
A later agent's search_memory surfaces those decisions
semantically — so the loop compounds knowledge instead of relearning it.
Agent interface
JSON-RPC 2.0 over POST /api/mcp, authed by a project-scoped
key. Every tool ships a typed outputSchema,
read-only / destructive annotations, and structured errors; creates take idempotency keys
and reads paginate.
| Tool | Kind | Purpose |
|---|---|---|
| Orientation | ||
| get_context | read | The key's project, scopes, project/tool counts — call first |
| list_projects | read | Project ids for the project_id override |
| Work queue | ||
| claim_next | write | Atomically claim the best ready item (leased) |
| next_cluster | write | Claim a whole code-neighborhood at once |
| heartbeat | write | Extend the lease on a claimed item |
| release_item | write | Return a claim to the queue |
| get_backlog | read | Prioritized backlog — ready-first, with blocked_by / unblocks / votes / score |
| suggest_next | read | Single best next item, dependency-aware |
| Items | ||
| create_item | write | Create a task (tags, touchpoints, PRD link) · idempotent |
| update_item | write | Patch / advance status |
| search_items | read | Query by text, tags, status (paginated) |
| get_item_details | read | Item + linked shards + requests |
| related_work | read | The code-neighborhood around a task |
| link_items | write | Typed link (dependency / code / semantic / tag) |
| Memory · Spec · Insight | ||
| add_memory | write | Attach a memory shard · idempotent |
| search_memory | read | Semantic (pgvector cosine) search over shards |
| decompose_prd | write | Spec sections → tracked tasks |
| prd_coverage | read | Per-section rollup + gaps |
| extract_lessons | write | Distill an item's lessons into memory |
| generate_digest | read | Progress digest across the project |
Persistence
Everything is project-scoped; items is the hub the loop turns on.
| Table | Carries |
|---|---|
| items | status, tags, effort, blocker · touchpoints (B) · assignee / claimed_by / claimed_at (A) · prd_id / prd_section (D) · github_url, pr |
| links | typed edges — dependency / code / semantic / tag (drives C's ready-set and B's clusters) |
| memory_shards | pgvector embeddings — semantic recall; auto-written on completion |
| prds · prd_versions | markdown specs with version history; sections drive coverage (D) |
| requests · attachments | public feedback triage — votes roll onto linked items (C); image attachments by id |
| api_keys | scoped to a project — one key = one agent |
| sync_state | per-PRD last-synced hash for Drive conflict detection |
| platform_config | per-project AI provider, integrations, spam settings |
Alembic chain 0001 → 0012 applies clean on a fresh database.
Reach
repository.full_name and creates the item in the project that has that repo connected, linking it back to the issue via github_url.PRDs/*.md against a SyncBackend interface. The filesystem backend works today; point the sync directory at a Drive Desktop folder and it reaches Drive with no OAuth. A per-PRD hash flags conflicts instead of clobbering; a native Drive-API backend drops into the same interface.Embedder / ChatModel / Extractor protocols. The stub is the offline default; Ollama, Claude, or OpenAI swap in by environment variable.Runtime
pgvector/pgvector:pg16 + api + nginx SPA). Migrations run on startup; the app starts empty and you sign up.