Technical deepdive
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
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.
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.
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
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.
| 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 |
| setup_project | read | First-run bootstrap — a resumable checklist that takes a fresh project from empty to useful; re-runnable, reports done/pending |
| 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, fidelity, PRD link) · idempotent |
| update_item | write | Patch / advance status · set fidelity |
| 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) |
| unlink_items | write | Remove a typed link — the inverse of link_items · idempotent |
| Memory · Insight | ||
| add_memory | write | Record a memory shard — enters as a candidate pending human publish · idempotent |
| search_memory | read | Semantic (pgvector cosine) search over published shards |
| extract_lessons | write | Distill an item's lessons into candidate memory |
| generate_digest | read | Progress digest across the project |
| PRDs · grill | ||
| create_prd | write | Author a PRD — the handoff artifact; ## sections drive decompose/coverage |
| update_prd | write | Patch a PRD's title / status / body |
| grill_prd | read | Next clarifying questions to sharpen a PRD before building |
| decompose_prd | write | Spec sections → tracked tasks (classified by fidelity) |
| prd_coverage | read | Per-section rollup + gaps + open high-fidelity work |
| Code graph | ||
| describe_code | write | The coding agent upserts the code map — nodes (module/file/symbol + summary) and typed edges; echoes the paths it touched |
| get_code_map | read | The project's described nodes + edges |
| code_neighbors | read | Edges around a path + the work items touching it |
| search_code | read | Semantic search over code-node summaries |
| link_code | write | Bridge an item/request to a code path (affects / implements / fixes / tests) |
| unlink_code | write | Remove an item/request ↔ code link |
| Upstream | ||
| report_agentledger_issue | write | Report 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
Authentication proves identity; authorization decides what an identity may touch — and the two are separate contracts. Capability and permission never get conflated.
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.
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
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.
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.
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
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 |
| code_nodes · code_edges · code_refs | the agent-described code graph — modules/files/symbols + typed edges, and item/request ↔ path bridges |
| events | append-only audit ledger — actor / action / target per accepted mutation |
| api_keys | scoped to a project + read/write scopes — enforced, one key = one agent |
| memberships | per-(user, project) role + access — the authorization source of truth |
| sync_state | per-PRD last-synced hash for Drive conflict detection |
| platform_config | per-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
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.
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.
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.
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
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.