Technical deepdive

How the loop is actually built.

AgentLedger is a FastAPI + Postgres/pgvector backend, a React SPA, and one 32-tool MCP — all sitting on a single shared service layer. Here's the architecture, the agent-loop mechanics, how authority is enforced and audited, and the data model that makes it work.

System topology

Two entry planes, one code path

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.

Web SPA
React 19 · TanStack · JWT
Agents / MCP clients
API key · JSON-RPC 2.0
↓ REST routers        MCP server (32 tools) ↓
Shared service layer
app/services/* — items · memory · prds · assistant · clustering · collision · prioritization · code_graph · events · quotas · orgs · drive_sync
↓ SQLAlchemy models · Alembic (0001 → 0034)
Postgres + pgvector
dialect-aware EmbeddingType · HNSW cosine index

Why the service layer is the keystone

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

The agent loop, mechanic by mechanic

prd_coverage
specced but unbuilt
decompose_prd
spec → tasks
get_backlog
dep-aware, code-local
claim_next
atomic, leased
update done
→ memory
A · Claiming & leases

Collision-free assignment

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.

B · Touchpoints & clusters

Batching by code-locality

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.

C · Prioritization

Priority from the dependency graph

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.

D · Grill & spec coverage

Grill the spec, then it drives the tracker

PRDs are authored and sharpened by grilling — an interactive interrogation (grill_prd / a streaming grill session) that asks relentless clarifying questions and classifies each open question low- vs high-fidelity (the latter needs a prototype: the grill → prototype → grill handoff). Every decision is preserved as a candidate memory shard. Then decompose_prd parses the spec's sections into a task per gap and prd_coverage returns the per-section rollup — status counts, percent done, and outstanding high-fidelity work.

Institutional memory you can trust

Completing an item auto-extracts its lessons into pgvector memory — but agent-written memory is telemetry, not truth. Lessons (and grill decisions) enter as candidates; a human publishes them before search_memory serves them, and recurring ones cluster into one principle. The loop compounds reviewed knowledge instead of letting a hallucination become ground truth.

Agent interface

The 32 MCP tools

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 idempotency keys on creates; reads paginate. Arguments are validated against the schema before dispatch, and failures return a typed code — not_found, validation, conflict, unauthorized, internal — with a hint naming the fix.

ToolKindPurpose
Orientation
get_contextreadThe key's project, scopes, project/tool counts — call first
list_projectsreadProject ids for the project_id override
setup_projectreadFirst-run bootstrap — a resumable checklist that takes a fresh project from empty to useful; re-runnable, reports done/pending
Work queue
claim_nextwriteAtomically claim the best ready item (leased)
next_clusterwriteClaim a whole code-neighborhood at once
heartbeatwriteExtend the lease on a claimed item
release_itemwriteReturn a claim to the queue
get_backlogreadPrioritized backlog — ready-first, with blocked_by / unblocks / votes / score
suggest_nextreadSingle best next item, dependency-aware
Items
create_itemwriteCreate a task (tags, touchpoints, fidelity, PRD link) · idempotent
update_itemwritePatch / advance status · set fidelity
search_itemsreadQuery by text, tags, status (paginated)
get_item_detailsreadItem + linked shards + requests
related_workreadThe code-neighborhood around a task
link_itemswriteTyped link (dependency / code / semantic / tag)
unlink_itemswriteRemove a typed link — the inverse of link_items · idempotent
Memory · Insight
add_memorywriteRecord a memory shard — enters as a candidate pending human publish · idempotent
search_memoryreadSemantic (pgvector cosine) search over published shards
extract_lessonswriteDistill an item's lessons into candidate memory
generate_digestreadProgress digest across the project
PRDs · grill
create_prdwriteAuthor a PRD — the handoff artifact; ## sections drive decompose/coverage
update_prdwritePatch a PRD's title / status / body
grill_prdreadNext clarifying questions to sharpen a PRD before building
decompose_prdwriteSpec sections → tracked tasks (classified by fidelity)
prd_coveragereadPer-section rollup + gaps + open high-fidelity work
Code graph
describe_codewriteThe coding agent upserts the code map — nodes (module/file/symbol + summary) and typed edges; echoes the paths it touched
get_code_mapreadThe project's described nodes + edges
code_neighborsreadEdges around a path + the work items touching it
search_codereadSemantic search over code-node summaries
link_codewriteBridge an item/request to a code path (affects / implements / fixes / tests)
unlink_codewriteRemove an item/request ↔ code link
Upstream
report_agentledger_issuewriteReport a bug/idea about AgentLedger itself; deduped on arrival

Every accepted write is metered and recorded to an append-only audit ledger, attributed to the calling key.

Trust boundary

Authority is enforced, and audited

Authentication proves identity; authorization decides what an identity may touch — and the two are separate contracts. Capability and permission never get conflated.

Scoped, not just labelled

A key can't out-rank its owner

Every mutation is bounded by the key's declared scopes and its owner's project memberships, checked in one place (security/authz.py) at both the MCP and REST boundaries. A project-scoped key can't reach another project's data — not even by passing a different project_id. Non-members get an existence-hiding 404; a read-only member gets an honest 403.

The ledger

Every change, on the record

One append-only events row per accepted mutation — actor (the agent key and the human principal behind it, or the user), action, target, project, timestamp — written at the boundary so attribution is never lost. The Activity view answers "what did my agents do, and who ran them" directly; auditing an authority model is what makes it a real control rather than a claim.

Human plane

An in-app assistant on the same rails

Beyond the agent loop, a person can open any item or PRD and chat with a model about it over SSE. The assistant is a provider-agnostic tool-calling layer: one internal contract that Anthropic's native tools and the OpenAI function-calling shape (OpenAI, xAI/Grok, Gemini, Ollama) both translate to, so any configured provider drives the same tools.

Propose-then-approve

The assistant can't write unattended

Tool calls that read run immediately; tool calls that write are staged as proposals gated to the caller's own authz — applied only on human approval, prior value captured for revert, and recorded in the same ledger with origin=assistant:<provider>. A prompt-injected item can, at most, propose a rejected action.

Per-thread model · metered

Bring your own key

Each conversation pins its own provider + model (Claude on one thread, Grok on another), keyed per project and encrypted at rest. Text streams token-by-token; input/output tokens are metered on the thread and counted against the org's call quota in hosted mode.

Persistence

Data model

Everything is project-scoped; items is the hub the loop turns on.

TableCarries
itemsstatus, tags, effort, blocker · touchpoints (B) · assignee / claimed_by / claimed_at (A) · prd_id / prd_section (D) · github_url, pr
linkstyped edges — dependency / code / semantic / tag (drives C's ready-set and B's clusters)
memory_shardspgvector embeddings — semantic recall; auto-written on completion
prds · prd_versionsmarkdown specs with version history; sections drive coverage (D)
requests · attachmentspublic feedback triage — votes roll onto linked items (C); image attachments by id
code_nodes · code_edges · code_refsthe agent-described code graph — modules/files/symbols + typed edges, and item/request ↔ path bridges
eventsappend-only audit ledger — actor / action / target per accepted mutation
api_keysscoped to a project + read/write scopes — enforced, one key = one agent
membershipsper-(user, project) role + access — the authorization source of truth
sync_stateper-PRD last-synced hash for Drive conflict detection
platform_configper-project AI provider, integrations, spam settings

Alembic chain 0001 → 0034 applies clean on a fresh database — and CI proves it on Postgres every change.

Fleet substrate

Point a subagent fleet at it

Cursor 3 — and any MCP client — now fans work across a fleet of cheap-model subagents, each running in its own isolated git worktree. Worktrees stop them clobbering each other at the byte level; they can't stop two agents from solving the same problem or grabbing the same work. That coordination gap is what AgentLedger fills. Connect over the same MCP endpoint — Cursor 3's Team MCP distribution then hands it to every cloud, IDE, and CLI agent on the team.

Curated context

Cheap models start oriented

A sessionStart hook injects the operating loop into an otherwise empty context window — the claimed item, the surrounding code neighborhood, the invariants that define done in this repo. A non-frontier subagent wakes up knowing what it's doing instead of guessing from the prompt.

Collision safety

Past the worktree boundary

Worktrees prevent byte-level clobber; leases prevent wasted work. claim_next and next_cluster hand each subagent a leased item — or a whole code-neighborhood — and an afterFileEdit hook flags any change straying outside it, so a fleet doesn't duplicate effort or drift onto another agent's files.

Portable agent files

One source, every client

A generator emits role-scoped subagent definitions — planner, implementer, scout, verifier — for Cursor, Claude Code, and Codex from a single source, with the repository's invariants baked into each. Regenerate on change; CI fails the build if they drift.

Reach

Integrations

Runtime

Stack & deployment