Design

Problem

AI agents call SQL through shell tooling, but classic terminal-first clients are not ideal for automated workflows:

  1. Output is human-formatted, not protocol-stable.
  2. Process-per-query overhead wastes latency and connection setup.
  3. Large SELECT * workloads need predictable streaming.
  4. Text interpolation patterns are easy to misuse and unsafe by default.

afpsql is an agent runtime for PostgreSQL: structured protocol, persistent connections, safe parameter binding, and Agent-First Data naming everywhere.

Product Boundary

afpsql has one runtime interface (Agent-First Data protocol). psql mode is only a CLI argument translation layer.

Non-goals:

Architecture is two CLI frontends -> one canonical agent-first execution core.

Execution layering:

Core Principles

  1. SQL-native protocol events.
  2. Agent-First Data naming conventions for fields and flags.
  3. Parameter binding for dynamic values.
  4. Structured errors with machine-readable codes.
  5. Large-result streaming as first-class behavior.
  6. psql mode performs argument translation only.
  7. production SQL execution path has no test failpoint semantics.
  8. No SQL-text heuristics for runtime behavior.
  9. Protocol events use stdout only; stderr is not a protocol channel.

Protocol Shape

Input commands:

Output events:

Parameter Binding (Required for Dynamic Values)

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 (validated from prepared-statement metadata, not by scanning SQL text).
  2. Invalid shape returns error_code: "invalid_params".
  3. Server-type conversion failures return error_code: "invalid_params".

Unsupported by design:

CLI Binding Forms

The agent-first CLI uses one canonical binding form:

CLI parsing translates this form into canonical protocol params array.

psql mode translation may accept numeric -v bindings:

afpsql --mode psql -c "select * from t where id = $1" -v 1=42

Translation rule: only numeric variable names are allowed; non-numeric names are rejected because interpolation is unsupported.

Modes

CLI mode

Single query execution and structured output.

Two parsers are available:

  1. agent-first parser (default)
  2. psql parser (--mode psql) -> translated into agent-first request

Pipe mode (--mode pipe)

Long-lived JSONL session on stdin/stdout:

Connection Model (Agent-First)

Connection may be supplied by:

  1. dsn_secret
  2. conninfo_secret
  3. discrete fields: host, port, user, dbname, password_secret

Optional environment fallback uses agent-first names:

Standard PostgreSQL fallback names are also supported (lower precedence):

Resolution precedence:

  1. request/session explicit fields
  2. translated CLI flags (agent-first or psql mode)
  3. environment fallback
  4. built-in defaults

Large Result Strategy

Runtime decision rule

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

afpsql must not parse SQL text (keyword scanning, placeholder text scans, etc.) to decide execution/output behavior.

Enforcement:

Inline path

Return one result if payload is below both limits:

Streaming path

When stream_rows=true:

  1. emit result_start with column metadata
  2. read PostgreSQL rows incrementally instead of buffering the full result
  3. emit repeated result_rows batches
  4. emit result_end with totals in trace

Batch controls:

If streaming is off and limits are exceeded, return:

Error Taxonomy

sql_error

PostgreSQL execution failure. Include sqlstate and SQL diagnostics.

error

Client/protocol/runtime failures. Always include:

Runtime diagnostics:

Agent-First Data Rules

Exit Codes (CLI)

MVP Scope

  1. query/config/cancel/ping/close
  2. result + result_* streaming
  3. connection resolution model above
  4. parameter binding (params)

Future: