The proposed lineage API, in the open.
How Lineleaf models artifacts and lineage, how invalidations are planned and executed, and what adapters and receipts look like — shared early so it can be shaped.
Nothing on this page is published yet. Package names are reserved; signatures will change as design partners use them. There is no install command because there is nothing to install.
Architecture
Lineleaf runs beside your application. Your writes go to your stores unchanged; Lineleaf receives lineage records and coordinates invalidation.
The app keeps writing to its own stores. The SDK or middleware reports each derivation to Lineleaf as it happens.
01
Sources
- Chats
- Emails
- Files
- Databases
- Tool output
02
AI application
- Agent
- Memory extraction
- Summarization
- Retrieval
- Caching
03
Lineleaf
- SDK
- Middleware
- Lineage API
- Invalidation planner
- Adapter coordinator
- Receipt generator
04
Stores
- Memory store
- Vector database
- Cache
- Search index
- Agent state
Proposed SDK interface
Three operations carry the model. Both SDKs share names and semantics.
| Operation | Records or returns | Side effects |
|---|---|---|
| derive(output, inputs, op, ttl?) | An edge from each input to the output | Writes lineage only |
| invalidate(source, reason) | A plan: every descendant and a proposed action | None until plan.execute() |
| why(artifact) | The ancestor path with ops, models, times, attribution | None |
lineage.py
from lineleaf import Lineleaf ll = Lineleaf(api_key=KEY) ll.derive( output=("mem0", mem_id), inputs=[ ("gmail", "msg_8812"), ("chat", "turn_331"), ], op="extract", ttl="180d",) plan = ll.invalidate( ("gmail", "msg_8812"), reason="user_erasure",) plan.affectedplan.execute() ll.why(("mem0", mem_id))lineage.ts
import { Lineleaf } from "lineleaf"; const ll = new Lineleaf({ apiKey: process.env.LINELEAF_KEY }); await ll.derive({ output: { store: "mem0", id: memId }, inputs: [ { store: "gmail", id: "msg_8812" }, { store: "chat", id: "turn_331" }, ], op: "extract", ttl: "180d",}); const plan = await ll.invalidate( { store: "gmail", id: "msg_8812" }, { reason: "user_erasure" },); plan.affected; // Artifact[] — nothing is touched yetawait plan.execute(); await ll.why({ store: "mem0", id: memId });Artifact and edge model
Lineleaf stores identifiers and relationships. The content of a memory, message, or embedding never needs to leave your stores.
Artifact
A stored object identified by store, ID, fingerprint, timestamps, and TTL.
- store
- mem0
- id
- mem_19c4
- fingerprint
- sha256:5b7d…
- created_at
- 2026-03-01T18:04:18Z
- ttl
- 180d
Edge
A parent-child relationship recording the operation, model, and time.
- parent
- gmail:msg_8812
- child
- mem0:mem_19c4
- op
- extract
- model
- extractor-v3
- at
- 2026-03-01T18:04:18Z
Event
An erase, update, stale, or poison trigger that walks through descendants.
- type
- erase
- source
- gmail:msg_8812
- reason
- user_erasure
- actor
- svc:privacy-bot
- scope
- planned · 9
Receipt
A signed, hash-linked record of every artifact and adapter action involved in cleanup.
- id
- rcpt_4be1c0
- event
- evt_93ac
- actions
- 9 rows
- chain
- prev → hash
- signature
- ed25519
Invalidation lifecycle
Every artifact moves through the same small state machine. Agents can subscribe to transitions.
- Active → Stale. An ancestor changed or was invalidated.
- Stale → Pending action. A plan assigns delete, rebuild, or review.
- Pending → Rebuilt. Regenerated from the parents that remain valid.
- Pending → Deleted. Removed by its adapter; recorded on the receipt.
Dry-run workflow
Destructive actions are always a two-step operation: plan, then execute. Scope limits stop a plan from silently growing between the two.
dry_run.py
plan = ll.invalidate(("gmail", "msg_8812"), reason="user_erasure", dry_run=True) for step in plan.affected: print(step.store, step.id, step.action) # nothing has been touched plan.require_scope(max_artifacts=50) # refuse silent expansionreceipt = plan.execute() # adapters act; receipt returnedreceipt.verify() # check signature and hash chaindry-run.ts
const plan = await ll.invalidate( { store: "gmail", id: "msg_8812" }, { reason: "user_erasure", dryRun: true },); for (const step of plan.affected) { console.log(step.store, step.id, step.action); // nothing has been touched} plan.requireScope({ maxArtifacts: 50 }); // refuse silent expansionconst receipt = await plan.execute(); // adapters act; receipt returnedawait receipt.verify(); // check signature and hash chainAdapter interface
An adapter declares what its store can guarantee. Stores that cannot confirm deletion are reported, not hidden.
adapter.ts
// Proposed adapter interface. Every store implements four verbs.export interface LineleafAdapter { store: string; // "pgvector", "redis", … capabilities: { delete: "guaranteed" | "best_effort" | "none"; rebuild: boolean; }; exists(ids: string[]): Promise<Record<string, boolean>>; delete(ids: string[], ctx: ActionContext): Promise<ActionResult[]>; markStale(ids: string[], ctx: ActionContext): Promise<ActionResult[]>; rebuild?(ids: string[], ctx: ActionContext): Promise<ActionResult[]>;}adapter.py
# Proposed adapter interface. Every store implements four verbs.class LineleafAdapter(Protocol): store: str # "pgvector", "redis", … capabilities: Capabilities # delete: guaranteed | best_effort | none def exists(self, ids: list[str]) -> dict[str, bool]: ... def delete(self, ids: list[str], ctx: ActionContext) -> list[ActionResult]: ... def mark_stale(self, ids: list[str], ctx: ActionContext) -> list[ActionResult]: ... def rebuild(self, ids: list[str], ctx: ActionContext) -> list[ActionResult]: ...| Adapter | Covers | Planned actions | Status |
|---|---|---|---|
| Mem0 | Long-term memories | delete · mark stale · rebuild | Planned |
| pgvector | Embeddings in Postgres | delete · re-embed | Planned |
| Redis | Caches and cached answers | delete (purge) | Planned |
| LangGraph Store | Agent state and long-term store | mark stale · delete | Planned |
| Custom | Any store you operate | implement the four verbs | Adapter interface |
Product names are trademarks of their owners. No partnership or endorsement is implied.
Receipt structure
Each action commits to the previous one, so a receipt cannot be edited without breaking the chain. Partial outcomes are recorded as such.
{ "receipt": "rcpt_4be1c0", "event": { "type": "erase", "source": "gmail:msg_8812", "reason": "user_erasure" }, "scope": { "planned": 9, "executed": 8, "needs_review": 1 }, "actions": [ { "seq": 1, "artifact": "mem0:mem_19c4", "action": "delete", "status": "completed", "prev": "b81f…", "hash": "07c2…" }, { "seq": 2, "artifact": "search:idx_ab12", "action": "delete", "status": "needs_review", "detail": "adapter capability: best_effort", "prev": "07c2…", "hash": "e45a…" } ], "signature": { "alg": "ed25519", "key": "org_key_01", "value": "…" }}Webhook concept
Live agents should not keep acting on stale context. Lineleaf can post a notice whenever something an agent depends on changes.
{ "type": "lineage.stale", "artifact": "langgraph:plan_17", "because": ["gmail:msg_8812"], "event": "evt_93ac", "action": "mark_stale", "hint": "re-plan before acting on this output"}Honest implementation status
Where things actually stand. This list is updated as work lands.
- Product concept and architectureDefined
- Proposed SDK interfaceDraft — this page
- Postgres lineage graphIn development
- Python SDKPlanned · package name reserved
- TypeScript SDKPlanned · package name reserved
- Dry-run invalidation plannerPlanned
- Signed receiptsPrototype planned
- First adaptersPlanned
- Blast-radius web interfaceConcept — demo on the home page
- Managed cloudLater