Skip to content

Architecture

  db-agent ask "..."            (cli/main.py)
        |
     run(question)              (run.py)  ->  AgentResult
        |
   run_react_loop               (agent/react_loop.py)   AgentState: hop_count, retry_count, failed
        |   ^
        |   +--- Claude tool-use turns (generation/claude_client.py, model claude-sonnet-5)
        v
   tool dispatch (arguments validated against each tool's exact key set)
     |- get_schema(table_name)         one table only, 300 s TTL cache
     |- execute_read_only_sql(query)   AST guard -> agent_ro session
     '- generate_chart(spec)           pydantic-validated Vega-Lite subset

   get_agent_session()   (db/session.py): the only place a connection is made
     agent_ro role + SET TRANSACTION READ ONLY   ->   Postgres 16 (Pagila)

The loop

Each iteration asks the model for one turn. A final answer ends the run. A tool call increments hop_count, runs, and its result (or error text, verbatim) is appended to the history as a tool_use / tool_result pair so the model can react. Limits, from agent/state.py:

Limit Value Behavior when reached
MAX_HOPS 8 tool calls run ends, failed=True, summary "stopped after N steps without a final answer"
MAX_RETRIES 3 consecutive tool errors run ends, failed=True, summary "gave up: ..."
statement timeout 3000 ms query returns an error observation

A successful tool call resets the consecutive-error count. Unknown tool names and wrong argument keys are error observations, not crashes. See ADR 0001.

Tools

  • get_schema: columns (name, type, nullability) and foreign keys for exactly one table in public. Foreign keys are read from pg_catalog because information_schema hides them from a SELECT-only role. There is no whole-catalog call, which bounds prompt size structurally.
  • execute_read_only_sql: guard first (ADR 0002), then the read-only session (ADR 0005).
  • generate_chart: ADR 0003.

Deployment shape

docker/docker-compose.yml defines project db-action-agent with postgres (Postgres 16, admin bootstrap role, host port 5433, init scripts in docker/init-db/) and agent (image with the db-agent entrypoint). There is no HTTP service (ADR 0004).