Tables / how it works
01 The big idea

The agent's harness is a database.

Most LLM agents hide their memory and control flow in a JavaScript object or a server. Tables does the opposite: the entire agent — its memory, its tool calls, its artifacts, and the loop that drives it — lives inside an in-browser SQLite database. You can SELECT * FROM messages and read the agent's whole mind.

user INSERT messages agent_think JSPI ⏸ LLM assistant INSERT + tool_calls execute_tool run UDF tool INSERT result loop — think → act → observe — until a final answer with no tool calls

The ReAct loop is not a while loop in JavaScript. It is a cascade of SQLite triggers. Drop a user message into messages and the database reacts to itself — thinking, acting, observing — until it stops. The only JavaScript that runs is the thin layer that does browser I/O and network requests. Everything else is declarative SQL living in the database.

02 The loop

Trace one turn, step by step.

Press play or click a step. Watch a single user message ripple through the trigger cascade, and watch the messages table fill in as each trigger fires.

What happens

SQL that runs


          

Live messages table

idrolecontent / tool_calls
03 The harness

Everything is a table.

Every piece of agent state is a row in a table you can query. Click any object to see what it is, why it exists, and the real DDL that creates it. Filter by category to focus.

← Select an object to inspect it.

04 Time travel

Rewind the data. Compact the memory.

Two independent systems give the agent a sense of time — one for undoing what it did to your data, one for forgetting what it no longer needs in context.

⟲ Rewind — undo a turn

Every turn runs inside an atomic SAVEPOINT turn_sp. Per-table capture triggers (cap_*) write a pre/post row image for every change into turn_changesets; DDL goes to turn_ddl_log with a pre-image. Every journaled write also carries a source — who made it (agent, scratchpad, app, engine, host) beside the turn id's when. Click the ⟲ on any message and the inverse of that conversation's own writes is replayed — data and schema return to that turn, the chat rewinds, and writes made by the interface are left alone (the confirm names them first). A 20-turn ring buffer bounds the cost.

turn 4 turn 5 turn 6 turn 7 turn 8 ⟲ replay inverse changesets

◐ Compact — shrink the context

When token usage nears the model's window, the oldest turns are summarized into a compactions row and a watermark advances. The v_active_context view simply stops reading rows below the watermark. The messages table is never touched — it stays a complete, immutable audit log.

summary watermark rows after the watermark are what the LLM sees
05 The boundary

The agent can't break its own harness.

A powerful agent that can run arbitrary SQL could also DROP TABLE messages and destroy itself. Tables draws a hard line: the app's own tables are protected from destructive DDL, so the agent can analyze your data without ever corrupting the machinery that runs it.

✓ Your data — fully writable

  • sample_data
  • any table you create
  • CSV imports
  • materialized tool output
  • your views
  • artifacts · artifact_styles
isProtectedTable()

⛔ The harness — read-only to the agent

  • messages · sessions · session_context
  • system_config · system_files · llm_profiles · tools
  • turn_changesets · turn_ddl_log · compactions
  • tool_approvals · dashboard_cards (retired)
  • documents · documents_fts

The guard lives in two places — the agent's run_dynamic_sql and the direct-SQL console's write gate — both call the same isProtectedObject() check. You can still query the harness (that's the whole point); you just can't drop or alter it.

06 The stack

Why this is even possible: JSPI.

The crux of the whole design is a single capability: a SQL function that can wait on the network and then resume. That's what JavaScript Promise Integration (JSPI) adds to WebAssembly — and it's what lets an LLM call happen inside a trigger.

1 Browser JS DOM, file I/O, the network — the only imperative layer
2 JSPI fibers suspend a WASM call on a promise, resume when it settles
3 SQLite (WASM) the engine — tables, views, triggers, UDFs
4 The harness agent_think · execute_tool · ask_llm · capture triggers

When agent_think calls the ask_llm UDF, the UDF makes an HTTP request to your LLM provider. Without JSPI, the WASM module would block the whole browser thread. With JSPI, the fiber pauses — the trigger is genuinely suspended mid-statement — and the browser is free to render, stream tokens, and handle clicks. When the LLM's JSON arrives, the fiber resumes and the trigger continues, inserting the assistant row and re-triggering the cascade.

-- inside the agent_think trigger, this one call suspends the whole WASM fiber:
SELECT ask_llm(
    (SELECT json_group_array(json_object(...)) FROM v_active_context),
    (SELECT json_group_array(json(schema)) FROM tools)
) AS llm_response;  -- ⏸ fiber parks here while the network round-trip happens