Skip to content

ask-rb/ask-state-providers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ask-state-providers

Pluggable state backends for the ask-rb ecosystem. Provides Ask::State::Adapter implementations for SQLite, Redis, PostgreSQL, and MySQL — one interface, four databases, zero coupling to your infrastructure.

# Local dev — zero config
store = Ask::State::Providers::SQLite.new

# In production with Rails
store = Ask::State::Providers::Redis.new(url: ENV["REDIS_URL"])

# With your existing database
store = Ask::State::Providers::Postgres.new(url: ENV["DATABASE_URL"])

Why?

ask-rb agents and sessions need to persist state — conversations, tool results, locks, task queues. Each deployment has different infrastructure: a CLI tool needs SQLite, a Rails app already has Postgres, a distributed system needs Redis. Instead of baking one backend into ask-core, this gem provides them all as drop-in adapters behind the same Ask::State::Adapter contract.

Installation

Add this line to your Gemfile:

gem "ask-state-providers"

Then add the database driver for the backend you want to use:

# For SQLite (ships with Ruby's standard library — no extra gem needed on most systems)
gem "sqlite3"

# For Redis
gem "redis"

# For PostgreSQL
gem "pg"

# For MySQL
gem "mysql2"

Quick Start

require "ask-state-providers"

# Pick your backend:
store = Ask::State::Providers::SQLite.new(path: "my_app.db")
# store = Ask::State::Providers::Redis.new(url: ENV["REDIS_URL"])
# store = Ask::State::Providers::Postgres.new(url: ENV["DATABASE_URL"])
# store = Ask::State::Providers::MySQL.new(url: ENV["MYSQL_URL"])

# Key-value storage
store.set("user:1", { name: "Alice", role: "admin" })
store.get("user:1")         # => {"name" => "Alice", "role" => "admin"}
store.set("temp", "expires", ttl: 3600)  # auto-expires in 1 hour
store.delete("user:1")

# Conditional create
store.set_if_not_exists("lock:deploy", "in_progress")

# Distributed locking
lock = store.acquire_lock("deploy-prod", ttl: 60)
store.release_lock("deploy-prod", lock) if lock

# Message queues
store.enqueue("tasks", { action: "send_email" })
task = store.dequeue("tasks")

# Ordered lists
store.list_append("recent_events", event, max_length: 100)
store.list_range("recent_events", 0, 9)  # first 10

Backends

SQLite (Ask::State::Providers::SQLite)

Best for single-process, single-user applications — CLI tools, local development, personal agents.

Feature Detail
Driver sqlite3
Configuration SQLite.new(path:)
Storage Single file on disk
Concurrency WAL mode with 5-second busy timeout
Tables state_store, locks, queues, lists (auto-created)

Uses INSERT OR REPLACE for key-value, INSERT ... WHERE NOT EXISTS for conditional writes, and DELETE ... RETURNING for safe queue dequeue.

Redis (Ask::State::Providers::Redis)

Best for distributed, multi-process, or multi-host deployments.

Feature Detail
Driver redis
Configuration Redis.new(url:)
Storage In-memory with optional persistence
Key prefix ask:state: (all keys are namespaced)

Leverages Redis-native primitives: SET NX EX for atomic locking with auto-expire, RPUSH/LPOP for FIFO queues, LTRIM for bounded lists, Lua EVAL for safe lock release.

PostgreSQL (Ask::State::Providers::Postgres)

Best for Rails apps and deployments already running Postgres.

Feature Detail
Driver pg
Configuration Postgres.new(url:)
Connection pool Built-in via connection_pool (default pool size: 5)

Uses ON CONFLICT, RETURNING, and INSERT ... WHERE NOT EXISTS for safe concurrent access.

MySQL (Ask::State::Providers::MySQL)

Best for teams already running MySQL or MariaDB.

Feature Detail
Driver mysql2
Configuration MySQL.new(url:)
Character set utf8mb4 (full Unicode including emoji)

Uses prepared statements, ON DUPLICATE KEY UPDATE, and SELECT ... LIMIT 1 for safe dequeue.

API Reference

All backends implement Ask::State::Adapter:

Key-Value

Method Description
get(key) Retrieve a value, or nil if missing or expired
set(key, value, ttl:) Store a value (JSON-serializable). ttl in seconds
delete(key) Remove a key
set_if_not_exists(key, value, ttl:) Create only if key doesn't exist (or is expired). Returns true/false
clear Remove all keys

Distributed Locking

Method Description
acquire_lock(key, ttl:) Acquire a lock. Returns Lock or nil
release_lock(key, lock) Release a lock (only the owner can). Returns true/false

Message Queues

Method Description
enqueue(queue, value) Push to the back of a queue. Returns QueueEntry
dequeue(queue) Pop from the front of a queue. Returns QueueEntry or nil
queue_depth(queue) Number of items in the queue

Ordered Lists

Method Description
list_append(key, value, max_length:) Append to list. Trims to max_length (keeps newest)
list_range(key, start, stop) Slice of the list. stop = -1 means all
list_remove(key, value) Remove all occurrences. Returns count removed

Lifecycle

Method Description
close Close the connection(s)

Using with ask-agent Sessions

require "ask-state-providers"
require "ask-agent"

store = Ask::State::Providers::SQLite.new

session = Ask::Agent::Session.new(
  "triage",
  model: "gpt-4o",
  state: store
)

session.run("What happened last time we saw this error?")
# Every turn is persisted — survive restarts, searchable, auditable

Development

# Install dependencies
bundle install

# Run tests (SQLite tests run everywhere, Redis needs fakeredis,
# Postgres/MySQL need DATABASE_URL/MYSQL_URL env vars)
bundle exec rake test

# Run tests with verbose output
bundle exec ruby -Itest test/ask/state/providers/sqlite_test.rb
bundle exec ruby -Itest test/ask/state/providers/redis_test.rb

# Test Postgres locally
DATABASE_URL="postgres://localhost:5432/ask_state_test" bundle exec rake test

# Test MySQL locally
MYSQL_URL="mysql2://root@localhost:3306/ask_state_test" bundle exec rake test

License

MIT — see LICENSE.

Links

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages