GNL
GNLDurable agents · edge-native

The process crashed. The card wasn't charged twice.

A durable agent framework — runtime, HTTP server, Studio, evals, auth. Nothing to rewrite: keep the agent you built on the Vercel AI SDK, call it again with the same runId, and completed tools never run again.

exactly-once32.4 KiB gzipyour data stays with youSQLite · Postgres · Redisserver · Studio · evals · RBAC
terminal
npm create gnl@latest
src/agent.ts
  import { generateText } from 'ai';
+ import { runDurable } from '@gnldev/durable';

- const res = await generateText({ model, tools, prompt });
+ const res = await runDurable({
+   runId, journal, model, tools, prompt,
+ });

Same arguments as generateText. The only additions are a runId and a journal — your tool definitions stay exactly as they are.

Works withVercel AI SDKOpenAIAnthropicPostgreSQLRedisSQLiteCloudflare Workers
1dependency in the core runtime
4,900+tests, run against real Postgres and Redis in CI
2/5 → 0/5the attacker's transfer landed — taint guard off, then on
0outbound calls — no telemetry, no phone-home
How each number was measured → the evidence on the compare page

You can miss it. Your model can miss it. GNL doesn't — it stops and asks.

Your AI said "created". Did it?

When a tool result is answered from the journal instead of executing, the model narrates it as a fresh success — and a user cannot tell the two apart. The guarantee was working; the narration was lying. We measured that in a live test; the mechanisms that close it, and what they cost, are below. And a real-world repeat rarely arrives with the same bytes: the hash catches the identical repeat; a repeat spelled differently can only be seen by the semantic identity layer — which never decides, it asks.

our own measurement · generated scenarios, real engine

The user asked for something new. The model re-emitted the previous turn's payment.

"…email a summary of this month's payments" → payInvoice({ ref: 'INV-7702', amount: 4250 })

This is not a user error: the human wrote the request correctly, and the model produced the previous turn's payment as the argument — no amount of care and no prompt closes that. In conversation traffic we ran through the real engine it happened in 13 of the 126 turns where the model called a tool; all 13 stopped at the gate and the decision went to a human — without the layer there would have been invoices paid twice and orders placed twice. In the same run the layer also asked 3 times when it should not have. We wrote the scenarios ourselves and ran a single tool model; this is not a rate for your deployment — what we measured is that the failure exists and the user cannot prevent it.

measured in a live test

The model said "order created" three times. The real counter stopped at 2.

The gap between the side-effect counter and what the model narrated was a lie the user could not see — an answer served from the record was indistinguishable from a fresh success. In GNL the model now says it itself: "the operation was not performed again — this is the record of the earlier one." Second live proof: "KLIMA-5" was ordered; when "klima-5" came in the hash missed — the semantic identity layer caught it and the question went to a human.

replayDisclosure: 'explain'

The score never decides — deterministic fields decide, a human has the last word.

Blind when deciding, honest when narrating

The note is injected only into the step AFTER the record is consumed; it never reaches the journal, thread memory, or any later turn. The model does not learn the work was already done BEFORE it decides to call — if it did, you would have silent, unauditable dedup.

Accidents are absorbed, everything else is always asked

A double-click or a repeat inside the retry window is absorbed without a question. A repeat outside that window — whether the user made it or the model did — becomes a human question EVERY TIME; approving it really does create the second job, denying it does not. No repeat is swallowed silently, and none runs silently.

Different words, same job — where the hash is blind

A double-click is caught by the hash; but the user spells it differently this time, or the model extracts the argument differently ("LAMBA-1" / "lamba-1", a unicode dash). At that moment the hash is blind, and the ONLY layer that can step in is semantic identity: the embedding merely finds CANDIDATES, normalized identity fields make the decision, and differing amounts are written onto the question. If the embedder is down the flow fails open — a human decides in every case.

The approval box shows the model's REAL arguments

In a live case the user said 250 and the model passed 100 — models can copy the amount sitting in the context rather than the one you asked for. The box shows the EXACT arguments the tool would run with, which is how a human caught it; a summarised sentence would not have.

We measured our limits and we publish them.

Semantic cost is thread-local: it grows with the number of side-effect jobs in ONE conversation, not with users, tenants, or total volume. A deployment with millions of users still pays tens of milliseconds per gated call; scaling out is the ordinary horizontal kind.

0.25 msper exact dedup read — practically free on the hot path
36 mssemantic scan in a conversation of 100 records
~650 msthe same scan at 2,000 records — linear, thread-local
1,612passing tests in the @gnldev/durable package alone

The boundary is stated plainly: a single conversation carrying thousands of side-effect jobs pushes the scan toward a second — that is the deliberate v1 boundary (in-process brute force, thread scope). An external vector index (v2) is deliberately waiting on data; because the recall interface already isolates the scan, it will be a swap rather than a rewrite.

Measured 6 September 2026 · real Postgres · 2048-dim vectors · bench-scale.ts

Open the duplicate-guard docs

What you build on the journal

Every capability below rests on the same journal — that is why each one is exactly-once and replayable, rather than a subsystem with its own rules.

@gnldev/durable

Exactly-once tools

Tool calls are keyed by the toolCallId the AI SDK generates. On replay the same id comes up, so a completed tool never runs again — the card isn't charged twice.

Open the docs ›
JOURNAL · append-onlymodel:0 ✓tool:call-3 ✓tool:call-7 chargeputIfAbsent → single winnerworker A · call-7✓worker B · call-7already recorded → skippedcard isn't charged twice — side effect exactly 1
charge-agent.ts
const journal = new InMemoryJournal();
const counter = { charges: 0 };

// 1) İlk çalıştırma: tool çağrılır, sonra çöker.
await expect(
  runDurable({
    runId: 'run-1', journal, model,
    tools: tools(), prompt: 'charge',
  }),
).rejects.toThrow('CRASH');
expect(counter.charges).toBe(1);

// 2) Aynı runId ile devam: chargeCard TEKRAR ÇALIŞMAZ,
//    sonuç journal'dan gelir.
const res = await runDurable({
  runId: 'run-1', journal, model,
  tools: tools(), prompt: 'charge',
});
expect(counter.charges).toBe(1); // exactly-once

How it works

From request to crash to replay — the journal captures every step, so a crashed process resumes right where it left off, with the same result.

01

A request comes in

`runDurable` is called — the agent loop starts with a runId and a prompt.

02

Appended to the journal

The model step and the tool call (keyed by toolCallId) are appended to the journal (Sqlite/Postgres/Redis).

03

The process crashes

Process kill, deploy, or timeout — everything in memory is lost.

04

Replay & resume

Called again with the same runId; the journal is replayed deterministically, and any tool that already completed never runs again.

See every run. Step back to any point.

Studio runs on top of the journal — there is no separate collection layer. That is why it stays complete after a crash: traces are derived from the journal after the fact, rather than collected live by a library that dies with the process.

The run, stopped at the step that matters

A real run from the support-desk example. The agent searched the policy, then called issueRefund — and the guard suspended the run before any money moved. The journal timeline sits under the approval banner, the time-travel scrubber is at step 3 of 4, and Fork @3 branches a what-if from exactly there. Cost reads $0.0000 because the example runs against a deterministic mock model, not a paid provider. — click the image to open it at full size.

All three screens were taken from the examples/app support desk in a single session, with no API key — you can reproduce them on your own machine.

approval queueaudit logretention / GDPR purgepolicy editorbudget & quota

Who it's for

GNL steps in for agent work where the side effects can't be undone.

Agents that process payments

A tool like chargeCard runs exactly once: even if the process crashes mid-run and gets called again with the same runId, the card isn't charged a second time.

Agents that send email or notifications

Tools like sendEmail and sendNotification are keyed by toolCallId — on resume, the user never gets the same notification twice.

Long-running workflows

Multi-step agent tasks resume from the last journal step even if they're interrupted by a deploy, restart, or timeout. In dynamic agent networks, routing decisions freeze into the same journal via CAS too — the router isn't called again on resume.

Durable tool calls

External API calls (payments, email, database writes) are appended to the journal and replayed idempotently — this guarantee holds for every tool you define.

Why GNL?

Orchestration is everywhere; durability isn't. GNL solves durability first, then builds everything else on top.

Take the guarantee without the framework

The core is one function over one dependency (superjson), and dependencies only ever point upward — the registry, the REST layer, workflows and Studio sit on top of it and are optional. Adopt runDurable alone, or the whole surface; drop the surface later and the guarantee stays where it was.

Your dashboard cannot stay green while the process is dead

Traces are derived from the journal after the fact rather than collected live, so they are complete even after a crash and identical across replays. A run that has not finished exports UNSET, never OK — which is precisely what an instrumentation library dying with its process cannot do.

Re-run yesterday's run against a new model

replayRun takes the input a recorded run froze and runs it again under a different model, prompt or tool set; diffRuns aligns the two by decision point and names the first one that changed. Strip what memory injected and the question "did recall cause this answer?" becomes an experiment instead of an assumption.

A durability stance, not just orchestration

Most agent frameworks focus on orchestration and bolt on durability later. In GNL, durability is the foundation: the journal comes first, orchestration sits on top of it — even dynamic agent-network routing and RAG queries are written durably to the same journal.

What GNL does not do — and what frameworks do better than us ›

Frequently asked questions

Is GNL an agent framework?

Both — and you decide how much of it you take. At the bottom sits runDurable: it takes the same arguments as generateText, leaves your tool definitions untouched, and brings one package with it (superjson). Exactly-once and deterministic replay come from there. Above it is an optional framework surface — an agent registry, automatic REST, workflows, agent networks, RAG — and above that, Studio and enterprise governance. You can stop at any layer, because dependencies only ever point upward: the guarantee lives at the bottom, not in the framework.

How is it different from a full agent framework?

Most agent frameworks are orchestration-first — workflow, RAG and memory — with durability added on top when it is offered at all. GNL starts from the other end: exactly-once and deterministic replay are the core guarantee, and everything else is built on the journal that provides them. That ordering is what makes the layers separable — you can take the guarantee without the framework, and drop the framework later without losing the guarantee. What we deliberately do not build: voice agents, ready-made channel integrations (Slack, WhatsApp) and a visual builder; if you need those, a full framework is the better tool. Chat UIs are supported — there are adapters for the Vercel AI SDK useChat and for AG-UI.

Do I need to change my existing Vercel AI SDK code?

No. runDurable takes the same arguments as generateText and returns the same result, plus an interrupts list — on the way in, the only additions are a runId and a journal. Your tool definitions stay exactly as they are; the runtime wraps them for durability itself.

What does exactly-once mean?

Tool calls are keyed by the toolCallId the AI SDK generates. Because the model's response is reproduced identically on replay, the same id comes up, so a completed tool never runs again.

Does my journal data go to GNL's servers?

No. The journal (the agent's run record) lives in your own database (BYO-DB). GNL doesn't host your data, and there's no payload telemetry or phone-home.

Which databases does it support?

SqliteStorage (single-process, dev/self-host) and network-based Postgres/Redis journal adapters (edge/serverless-compatible) are supported.

Does it run on edge/serverless?

With a 32.4 KiB core runtime and Postgres/Redis journal adapters, it embeds into edge environments like Cloudflare Workers with no cold-start cost.

Does it support multi-agent and RAG scenarios?

Yes, under the same journal guarantee: alongside static agent-as-tool, there are dynamic agent networks too — a router LLM decides which sub-agent runs each turn, decisions freeze into the journal via CAS, and the router isn't called again on resume. On the RAG side, chunking, a persistent pgvector store, and similarity-graph (GraphRAG) retrieval all run under the same exactly-once guarantee (see /docs/agent-networks, /docs/rag-pipeline).

What is Studio?

A visual ops panel that shows runs, the timeline, and time-travel replay. It also handles organizations, budgets, users, policy, and eval management.

Make your agent durable today.

Ship to production with exactly-once and deterministic replay — the journal stays in your own database.