org.llm4s.agent.graph.postgres

Members list

Type members

Classlikes

@Experimental
final class PostgresCheckpointer extends Checkpointer, AutoCloseable

A durable org.llm4s.agent.graph.Checkpointer in a PostgreSQL database: threads, their checkpoint histories, pending writes, event logs, effect records and run claims, shared by every runtime on every machine that opens a store over the same database and schema. It is the store for running one agent service on several machines (Stage 5, design 4.50).

A durable org.llm4s.agent.graph.Checkpointer in a PostgreSQL database: threads, their checkpoint histories, pending writes, event logs, effect records and run claims, shared by every runtime on every machine that opens a store over the same database and schema. It is the store for running one agent service on several machines (Stage 5, design 4.50).

Open it with PostgresCheckpointer.open, over a pool of its own, or PostgresCheckpointer.create, over a DataSource the caller already has; close it with close. It meets the Checkpointer contract and passes CheckpointerContract from llm4s-agent-testkit:

  • '''Atomic, fenced commits.''' Every call that writes is one transaction, which first locks the thread's row (SELECT ... FOR UPDATE) and then checks the claim's token, a Commit.fence's claim on another thread, and the parent checkpoint, before it allocates event numbers from the row and writes. A call that locks two threads' rows
    • a fenced commit or delete - locks them in the order of their ids, so two such calls never deadlock.
  • '''The database decides time.''' A claim's expiry and a retention policy's age are judged by the database server's clock (clock_timestamp(), read once the call holds its locks), never a host's: skew between the machines sharing the store changes nothing. Age is measured from the commit time the store stamps, by that clock, on every checkpoint and event row - never from the producer's Checkpoint.createdAt or event timestamp, which stay as the producer's record. The clock a runtime is given still schedules its renewals.
  • '''Fencing tokens''' come from one sequence for the whole schema, so they increase across restarts, releases, takeovers and deleteThread, and are never issued twice.
  • '''History, retention, deletes and children''' behave exactly as in the SQLite store: every superseded checkpoint is kept until prune, retractTurn or deleteThread removes it; a deleted thread leaves a tombstone of its event numbering; a parent's children are listed in the order their first commits landed.
  • '''Data only.''' Checkpoints, pending writes, events and effect records are stored as JSON text and decoded on read, so nothing executable is stored.
  • '''Change notification by polling''' for now: a subscription reads the log once per WatchPolicy.pollInterval (the Checkpointer.awaitEventsAfter default). LISTEN/NOTIFY replaces it in a later slice.

Reads run in a REPEATABLE READ, read-only transaction, so what one call reads is one snapshot; they never wait for a writer.

'''Every call returns within a bound''', as the Checkpointer contract requires (#1919): the pool's connection timeout plus PostgresCheckpointerConfig.callTimeout, plus the time a rollback takes. The call timeout is set on the server for the transaction (statement_timeout, lock_timeout, idle_in_transaction_session_timeout) and enforced on the client as well: a call that reaches it has its connection aborted and discarded from the pool, so a stalled network cannot hold it either. A call that reaches the bound fails with a ProcessingError. Like any networked store, a call that times out while its COMMIT is in flight cannot know whether the commit landed; the claim's fencing keeps that safe, as a later commit on a stale view is refused.

'''Interrupts.''' An interrupt flag already set when a call begins decides nothing: it is cleared for the call and set again when the call returns. An interrupt that lands during a call (on a virtual thread, it closes the connection's socket) ends that call with CancelledError or a failed result, and the connection is discarded, so the store stays usable by every other thread.

Every failure of the database itself is a ProcessingError, and so is any call after close. Safe to call from any number of threads: each call takes a connection of its own from the pool.

Attributes

Companion
object
Supertypes
trait AutoCloseable
trait Checkpointer
class Object
trait Matchable
class Any

Attributes

Companion
class
Supertypes
class Object
trait Matchable
class Any
Self type
@Experimental
final case class PostgresCheckpointerConfig

How a PostgresCheckpointer uses its database.

How a PostgresCheckpointer uses its database.

  • schema: the PostgreSQL schema its tables live in, llm4s by default, so the store can share a database with other tables (and two stores, in two schemas, can share one database without seeing each other). It is created if it does not exist. A lower-case SQL identifier: a letter or _, then letters, digits or _, at most 63 characters.
  • callTimeout: how long one call may hold its connection - every statement it runs, the waits for row locks another instance holds included - before it fails, changing nothing. It is set on the server as the transaction's statement_timeout, lock_timeout and idle_in_transaction_session_timeout, and enforced on the client too: a call still running when it passes has its connection aborted and discarded, so a call whose network stalls fails at the bound rather than waiting for the socket.
  • connectionTimeout: how long a call waits for a connection from the store's own pool (see PostgresCheckpointer.open); at least 250 milliseconds, HikariCP's minimum. A caller-supplied pool (PostgresCheckpointer.create) keeps its own setting, which then takes this one's place in the bound.
  • maxPoolSize: the size of the store's own pool; ignored for a caller-supplied one.

'''The bound.''' Every call returns within connectionTimeout + callTimeout, plus the time it takes to roll back or discard a connection: the bound the Checkpointer contract asks a store to document (#1919). Keep it well inside ClaimPolicy.ttl - ClaimPolicy.renewEvery (the defaults, 5 + 5 seconds, are inside 30 - 10).

Durations are rounded up to whole milliseconds and must be positive and at most Int.MaxValue milliseconds; apply and the with* setters throw IllegalArgumentException for an invalid value, and PostgresCheckpointerConfig.of returns a ValidationError.

Attributes

Companion
object
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type