Atomic commits
A common workflow: draft an invoice, attach three line items, mark it sent. Done as four separate tool calls, that's four R2 writes, four commit-log entries, four lines in the audit log, and an intermediate state where total=0 was briefly real.
That's not how bills work.
The transaction primitive
GitStore.transaction(message, fn) is a primitive that defers the commit:
const { result, commit, records } = await store.transaction(
"create Invoice with 3 lines",
async (tx) => {
const invoice = await tx.create(INVOICE_ENTITY, {
tenantId: "acme",
fields: { customer_id, total: 1850 },
});
const lines = [];
for (const line of incomingLines) {
lines.push(await tx.create(INVOICE_LINE_ITEM_ENTITY, {
tenantId: "acme",
fields: {
invoice_id: invoice.id,
description: line.description,
qty: line.qty,
unit_price: line.unit_price,
line_total: round2(line.qty * line.unit_price),
},
}));
}
return { invoice, lines };
},
);
tx.create() writes the record to the backend immediately so it isn't lost on power-down. It does NOT commit. After fn resolves, the transaction emits a single commit() call against the backend with all the paths the inner writes touched.
The audit log shows one entry, not four:
3a4f8b2 create Invoice with 3 lines
Christopher Bender, just now
Files changed:
data/acme/Invoice/8f3b2e1a-...agi
data/acme/InvoiceLineItem/c91d7e5f-...agi
data/acme/InvoiceLineItem/ad9320de-...agi
data/acme/InvoiceLineItem/64bbf4f3-...agi
The user-facing tool
POST /tools/create_invoice_with_lines
{
"tenant_id": "acme",
"customer_id": "cus_001",
"lines": [
{ "description": "Consulting hours", "qty": 10, "unit_price": 150 },
{ "description": "Travel", "unit_price": 350 },
{ "description": "Discount", "unit_price": -100 }
],
"notes": "Q3 services"
}
The Worker:
- Validates the lines array. Non-empty, every line has a
descriptionandunit_price.qtydefaults to 1. - Computes the invoice total as
sum(qty × unit_price). The agent doesn't pass total. The model doesn't do arithmetic. The DSL declarestotal: float REQUIREDand the executor satisfies it. - Runs the transaction.
- Upserts every produced record into the projection so the next query reflects them.
- Returns
{ invoice, lines, commit_oid }so the caller can confirm what was created.
Small models fill the array correctly more often than they fill four sequential tool calls correctly. Atomicity at the protocol level helps the agent get it right.
The failure mode
If fn throws, the transaction skips the commit. The records that were written to R2 still exist — R2 has no transaction primitive — but the audit log has no entry for them. Effectively, they're orphans: no one reads them, no projection upserts them, no GitHub commit references them. They're recoverable forensically but invisible operationally.
A future tightening: stage writes to a tenant-scoped scratch prefix during a transaction, atomic-rename to the real prefix on commit. R2's API doesn't have atomic rename, but a Durable Object could provide the lock. See roadmap.
Where this leads
The transaction primitive is the foundation for general-ledger correctness. The next entity to land here is JournalEntry with JournalLine children, where the transaction enforces that debits and credits sum to zero before the commit fires. If they don't, the transaction throws and the books stay balanced. That's accounting at the API layer.