Design

Purpose: a database contract agents can rely on

afpsql is a reliability layer between AI agents and PostgreSQL. It gives an agent a stable operational contract: structured events, explicit permission boundaries, predictable session state, and machine-readable failures.

It is not designed as a general-purpose high-performance pooler. Backend reuse is used to make session semantics reliable for agents, not to promise throughput, fair scheduling, or database-side load balancing.

The problem: terminal clients make agents guess

Agents often reach PostgreSQL through terminal tooling built for humans. That creates brittle automation:

  1. Results are rendered as text tables instead of typed records.
  2. Errors are prose instead of structured data with stable codes.
  3. Writes can happen accidentally when a generated SQL statement is wrong.
  4. Remote/container access often pushes agents toward SSHing into servers or using container exec commands for human psql.
  5. Stateful multi-step work is unclear when every command opens an unrelated backend session.
  6. Large results can flood stdout instead of using a bounded protocol.

afpsql addresses these with one Agent-First Data runtime protocol.

Product boundary: one runtime, several entry points

afpsql has one runtime interface. All execution modes feed the same agent-first core.

Modes:

Non-goals:

Core principles: reliability over throughput

  1. Reliability over throughput.
  2. Structured stdout events are the protocol; stderr is not a runtime channel.
  3. Native/pipe writes require explicit permission.
  4. PostgreSQL SQLSTATE is preserved for database errors.
  5. Permission, validation, transport, and protocol errors include actionable hints.
  6. Dynamic values use PostgreSQL parameters, never client text interpolation.
  7. Session state is explicit: pipe named sessions are stable backend sessions.
  8. Per-session query execution is FIFO.
  9. Runtime behavior is based on PostgreSQL metadata, not SQL-text heuristics.
  10. SSH and container boundaries are first-class transports, including --ssh + --container for containers on SSH hosts.
  11. psql mode is compatibility translation only; it does not fork runtime semantics.

Execution architecture: translate inputs, preserve one contract

High-level layering:

The user-facing model should stay simple: an agent sends SQL plus params and gets structured events back.

Permission model: writes cross an explicit boundary

Native CLI and pipe mode resolve a permission value for each query:

TransportDefaultWrite permission
direct PostgreSQL connectionreadwrite
afpsql SSH transportssh-readssh-write
afpsql container transportcontainer-readcontainer-write

Read permissions start PostgreSQL read-only transactions. A write attempt in that transaction fails as a sql_error with SQLSTATE 25006.

Permission values are intentionally tied to transport:

Mismatches fail before execution as invalid_request with a hint that tells the agent which permission family to use.

--ssh and --container can be combined to run the selected container driver on the SSH host. The database boundary is still the container, so the session uses container permissions rather than SSH permissions.

psql mode keeps psql’s writable default for script compatibility and does not expose native permission flags.

Session semantics: named sessions mean backend state

A pipe named session is intended to correspond to one PostgreSQL backend session for as long as the session config remains valid and the process remains alive. This lets agents rely on PostgreSQL session state, including temp tables and session-level settings, within that named session.

Rules:

This is a reliability contract. It should not be documented as a pooler or a performance feature.

Protocol shape: every recoverable outcome is an event

Input commands:

Output events:

Every recoverable runtime condition should be represented by one of these stdout events. Startup argument parsing can still exit with code 2, but it should use structured CLI error output when possible.

Parameter binding: values never become SQL text

When values are dynamic, clients should use $N placeholders and params.

{"code":"query","id":"q1","sql":"select * from users where id = $1","params":[123]}

Validation rules:

  1. Placeholder count must match params length, using prepared-statement metadata.
  2. Invalid client-side shapes or local binding conversions return error_code:"invalid_params".
  3. PostgreSQL server conversion/execution failures remain sql_error events with the original SQLSTATE.

Unsupported by design:

Result handling: bounded inline, explicit streaming

Row/command behavior is decided from PostgreSQL statement metadata after prepare:

Inline results are bounded by:

When streaming is enabled:

  1. emit result_start with column metadata
  2. read PostgreSQL rows incrementally
  3. emit repeated result_rows batches
  4. emit result_end with totals in trace

If streaming is off and limits are exceeded, the inline collector stops accepting rows at the cap and the result event carries truncated:true plus truncated_at_rows or truncated_at_bytes. The underlying statement still ran in full: for UPDATE ... RETURNING, the writes happened and the RETURNING projection is what got capped. Agents that need the full set should narrow the query or switch to --stream-rows.

Error taxonomy: SQLSTATE or actionable runtime code

sql_error

PostgreSQL execution failure. Include:

Agents should branch on sqlstate, not parse message text.

error

Client/protocol/runtime failure. Include:

Permission mismatches, validation errors, connection setup failures, unsupported TLS settings, and SSH transport validation should use this path. Connection-stage PostgreSQL failures still use code:"error" with error_code:"connect_failed", but should preserve SQLSTATE diagnostics when available so agents can distinguish authentication, role, database, capacity, and startup failures without parsing prose.

psql compatibility boundary: scripts yes, terminal semantics no

psql mode exists to let non-interactive scripts use familiar flags while receiving structured afpsql events.

It may translate:

It must reject or mark unsupported:

Implementation guardrails: protect the reliability contract

Skill design: behavior, not flag reference

skills/agent-first-psql.md is loaded by Claude Code and Codex as the agent’s behavior contract when working with afpsql. Its audience is users of the installed binary — the source tree and docs/reference.md may not be present on the user’s machine, but afpsql --help always is. The skill is shaped around that asymmetry.

Keep in the skill:

Drop from the skill:

For anything in the “drop” list, the agent runs afpsql --help or afpsql --help --recursive --output markdown to discover the current flag surface. Mirroring --help content into the skill makes it rot every release, bloats agent context, and trains the agent on stale flag names.

This sits alongside the existing “skip routine afpsql --help preflight before known query forms” guidance: the agent should not help-probe for known query shapes, but should help-probe for unknown flag detail rather than reading it from the skill.