Runs and the SDKs
Build it into your product. The TypeScript and Python SDKs give each task your agents take on a run: a branch of its own, with limits already set. Merge it, send it for review, or let it go.
npm install @bitsinfra/gnomon # TypeScript
pip install gnomon-postgres # PythonThe SDKs create no agents and call no model. They connect Gnomon to the application, the drivers and the agent framework you already have.
A run
import { Gnomon } from "@bitsinfra/gnomon";
import { nodePostgres } from "@bitsinfra/gnomon/pg";
const gnomon = new Gnomon({
app: nodePostgres(appPool), // your application's own login
runs: nodePostgres(runsPool), // one login shared by every agent run
});
await using run = await gnomon.startRun({
task: "Refund order 4182",
actor: "support-bot",
});
await run.sql("update orders set status = 'refunded' where id = 4182");
const { checks, changes } = await run.summary();
if (checks.passed && changes.rows <= 20) await run.merge();
else await run.submit({ title: "Refund order 4182" });The same thing in Python:
from gnomon import Gnomon
gn = Gnomon(app="dbname=shop user=shop_app", runs="dbname=shop user=shop_runs")
with gn.start_run("Refund order 4182", actor="support-bot") as run:
run.sql("update orders set status = 'refunded' where id = 4182")
summary = run.summary()
if summary["checks"]["passed"] and summary["changes"]["rows"] <= 20:
run.merge()
else:
run.submit("Refund order 4182")Leaving the block without merge() or submit() discards the branch, so an abandoned task leaves
nothing behind.
Two logins
A run is safe because of how it connects, not because of what the agent is asked to do.
- app is your application's own login, a member of
gnomon_runner. It starts runs, reads diffs and merges. - runs is one restricted login every run shares. Each statement goes out in a small envelope: begin on the run's branch, attribution, timeouts, one statement. The database refuses a second statement in the envelope, any write outside the run's branch, reads of other runs' branches, reads of the real tables, and merges — whatever SQL arrives.
select gnomon.set('runs_role', 'app_runs');
select gnomon.confine('app_runs', array[]::text[], false);Runs can't read each other's branches even though they share that login, because each transaction is bound to one run.
What a run can do
run.sql(statement, params?) | One statement on the run's branch; rows are capped, and a serialization failure is retried. |
run.transaction(fn) | Several statements in one envelope. |
run.diff(), run.summary(), run.checks() | What changed, typed per table (gnomon types --lang ts). |
run.merge(), run.submit(), run.discard() | Into the real data, to a person, or away. |
run.checkpoint(name), run.rewind(name), run.dryRun(...) | Savepoints across calls; try a change without keeping it. |
run.url(), run.connection(connect) | A connection string of the run's own, for a tool that brings its own connection (a migration runner, a BI client). |
Your agent framework
The same few tools — explore the schema, run SQL, see the diff, try a change without keeping it, checkpoint, rewind, finish — are exported for the framework you use, each bound to one run:
| TypeScript | @bitsinfra/gnomon/anthropic, /claude-agent, /ai (Vercel AI SDK), /openai-agents, /langchain, /mcp |
| Python | gnomon.tools.anthropic_tools(run), openai_agents_tools(run), langchain_tools(run), mcp_handler(run) |
Or hand the agent a connection string and no library at all — it is only Postgres. The MCP server →
Drivers and ORMs
Runs work through the drivers you already use, and the bindings put ordinary sessions on a branch:
node-postgres, postgres.js, Prisma, Drizzle and Kysely; psycopg, asyncpg, SQLAlchemy and Django;
pg and ActiveRecord; pgx and database/sql; JDBC, HikariCP and Spring; Postgrex and Ecto; Npgsql
and EF Core; PDO and Laravel.
TypeScript → · Python → · The wire protocol every SDK implements →
Your own review UI
The Approve, Reject and Undo buttons in your own product call the same review as the built-in app:
const requests = await gnomon.requests.open();
await gnomon.requests.get(id).approve();
await gnomon.requests.get(id).merge();In your tests
Each test gets its own branch of one seeded database, and can assert exactly what it changed. Testing →