ask-state-providers 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4af42039dcfbb213799ab09a162acb60c1e78f1b09cb94e52d9dd2bece7b18c
4
- data.tar.gz: b0deb44a8f91e0c6cc36dcabf279d822b08386ef49854225b78dc4955cc723b0
3
+ metadata.gz: 128f88b945a38f01707b8bac7bb1d78e85ffff581c392894346cdace4aad4f64
4
+ data.tar.gz: 0fae335db04415df0ebb3482afc5f132f83fe6822a988787f4cc10828d979197
5
5
  SHA512:
6
- metadata.gz: 53bf5a675035408f401a354f0ee3c778ac1c23dcd9115d54b192e65ddcd3df7b64a6ff0bf69aa9cc9b879cf7b485ed59ce2a79da15d8cee9b07688b03a66d5b1
7
- data.tar.gz: '09c163faa51ecfb38db57c93fcd33752213e8cfe4367706167cabf3e54130b88b7b6dfebd9e9ee14571b30ac839418fac3fafa06daccf9ae5729739d4716f7e1'
6
+ metadata.gz: d266562afb53d4e22cb30b340e5e973cc88962386baa295f430a0dcd53873562b8e0f0029215dfbbe241e3a62d11187e09b9eadc546e89517cffb70e9b28cf61
7
+ data.tar.gz: 37a5a82527b15ab1837f8ebf5c918af68de9e504b70f773b615bfa7d2adbe1700007b0df482428aa8fe709cb95ebd6215aa8b014133c54e842e28e5e8df7d797
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.4.1] — 2026-08-12
6
+
7
+ ### Fixed
8
+
9
+ - **`delete(key)` now removes everything under the key** — including
10
+ ordered lists (and queues in the Memory backend). Consumers store event
11
+ feeds as lists (ask-workflow's project store, the app-server session
12
+ store); previously the feed survived deletion. New shared contract test
13
+ `test_delete_removes_list_entries` runs against every provider.
14
+
5
15
  ## [0.3.0] — 2026-07-28
6
16
 
7
17
  ### Added
data/README.md CHANGED
@@ -1,44 +1,22 @@
1
1
  # ask-state-providers
2
2
 
3
- Pluggable state backends for the [ask-rb](https://github.com/ask-rb) ecosystem. Provides `Ask::State::Adapter` implementations for **SQLite**, **Redis**, **PostgreSQL**, and **MySQL** — one interface, four databases, zero coupling to your infrastructure.
3
+ [![Gem Version](https://badge.fury.io/rb/ask-state-providers.svg)](https://badge.fury.io/rb/ask-state-providers)
4
4
 
5
- ```ruby
6
- # Local dev — zero config
7
- store = Ask::State::Providers::SQLite.new
8
-
9
- # In production with Rails
10
- store = Ask::State::Providers::Redis.new(url: ENV["REDIS_URL"])
11
-
12
- # With your existing database
13
- store = Ask::State::Providers::Postgres.new(url: ENV["DATABASE_URL"])
14
- ```
15
-
16
- ## Why?
17
-
18
- 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.
5
+ Pluggable state backends for the ask-rb ecosystem. One `Ask::State::Adapter` contract, five backends: in-memory Memory, SQLite, Redis, Postgres, and MySQL. `Ask::State::Memory`, the in-process default, lives in this gem since 0.3.0.
19
6
 
20
7
  ## Installation
21
8
 
22
- Add this line to your `Gemfile`:
23
-
24
9
  ```ruby
25
10
  gem "ask-state-providers"
26
11
  ```
27
12
 
28
- Then add the database driver for the backend you want to use:
13
+ Add the driver gem for the backend you use:
29
14
 
30
15
  ```ruby
31
- # For SQLite (ships with Ruby's standard library — no extra gem needed on most systems)
32
- gem "sqlite3"
33
-
34
- # For Redis
35
- gem "redis"
36
-
37
- # For PostgreSQL
38
- gem "pg"
39
-
40
- # For MySQL
41
- gem "mysql2"
16
+ gem "sqlite3" # SQLite
17
+ gem "redis" # Redis
18
+ gem "pg" # PostgreSQL
19
+ gem "mysql2" # MySQL
42
20
  ```
43
21
 
44
22
  ## Quick Start
@@ -46,175 +24,53 @@ gem "mysql2"
46
24
  ```ruby
47
25
  require "ask-state-providers"
48
26
 
49
- # Pick your backend:
50
- store = Ask::State::Providers::SQLite.new(path: "my_app.db")
27
+ store = Ask::State::Memory.new # in-process, no persistence
28
+ # store = Ask::State::Providers::SQLite.new(path: "sessions.db")
51
29
  # store = Ask::State::Providers::Redis.new(url: ENV["REDIS_URL"])
52
30
  # store = Ask::State::Providers::Postgres.new(url: ENV["DATABASE_URL"])
53
31
  # store = Ask::State::Providers::MySQL.new(url: ENV["MYSQL_URL"])
54
32
 
55
- # Key-value storage
56
- store.set("user:1", { name: "Alice", role: "admin" })
57
- store.get("user:1") # => {"name" => "Alice", "role" => "admin"}
58
- store.set("temp", "expires", ttl: 3600) # auto-expires in 1 hour
59
- store.delete("user:1")
60
-
61
- # Conditional create
33
+ store.set("user:1", { name: "Alice", role: "admin" }, ttl: 3600)
34
+ store.get("user:1") # => { "name" => "Alice", "role" => "admin" }
62
35
  store.set_if_not_exists("lock:deploy", "in_progress")
63
-
64
- # Distributed locking
65
- lock = store.acquire_lock("deploy-prod", ttl: 60)
66
- store.release_lock("deploy-prod", lock) if lock
67
-
68
- # Message queues
36
+ store.acquire_lock("deploy-prod", ttl: 60)
69
37
  store.enqueue("tasks", { action: "send_email" })
70
- task = store.dequeue("tasks")
71
-
72
- # Ordered lists
73
- store.list_append("recent_events", event, max_length: 100)
74
- store.list_range("recent_events", 0, 9) # first 10
38
+ store.dequeue("tasks")
39
+ store.list_append("recent_events", "event-1", max_length: 100)
40
+ store.list_range("recent_events", 0, 9)
41
+ store.delete("user:1")
75
42
  ```
76
43
 
77
44
  ## Backends
78
45
 
79
- ### SQLite (`Ask::State::Providers::SQLite`)
80
-
81
- Best for single-process, single-user applications CLI tools, local development, personal agents.
82
-
83
- | Feature | Detail |
84
- |---------|--------|
85
- | **Driver** | [`sqlite3`](https://github.com/sparklemotion/sqlite3-ruby) |
86
- | **Configuration** | `SQLite.new(path:)` |
87
- | **Storage** | Single file on disk |
88
- | **Concurrency** | WAL mode with 5-second busy timeout |
89
- | **Tables** | `state_store`, `locks`, `queues`, `lists` (auto-created) |
90
-
91
- Uses `INSERT OR REPLACE` for key-value, `INSERT ... WHERE NOT EXISTS` for conditional writes, and `DELETE ... RETURNING` for safe queue dequeue.
92
-
93
- ### Redis (`Ask::State::Providers::Redis`)
94
-
95
- Best for distributed, multi-process, or multi-host deployments.
96
-
97
- | Feature | Detail |
98
- |---------|--------|
99
- | **Driver** | [`redis`](https://github.com/redis-rb/redis-rb) |
100
- | **Configuration** | `Redis.new(url:)` |
101
- | **Storage** | In-memory with optional persistence |
102
- | **Key prefix** | `ask:state:` (all keys are namespaced) |
103
-
104
- 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.
105
-
106
- ### PostgreSQL (`Ask::State::Providers::Postgres`)
107
-
108
- Best for Rails apps and deployments already running Postgres.
109
-
110
- | Feature | Detail |
111
- |---------|--------|
112
- | **Driver** | [`pg`](https://github.com/ged/ruby-pg) |
113
- | **Configuration** | `Postgres.new(url:)` |
114
- | **Connection pool** | Built-in via `connection_pool` (default pool size: 5) |
115
-
116
- Uses `ON CONFLICT`, `RETURNING`, and `INSERT ... WHERE NOT EXISTS` for safe concurrent access.
46
+ | Backend | Constructor | Notes |
47
+ |---|---|---|
48
+ | `Ask::State::Memory` | `Memory.new` | In-memory, thread-safe, lost on process exit |
49
+ | `Ask::State::Providers::SQLite` | `SQLite.new(path: "sessions.db")` | Single file, WAL mode, tables auto-created |
50
+ | `Ask::State::Providers::Redis` | `Redis.new(url:)` | Keys namespaced under `ask:state:` |
51
+ | `Ask::State::Providers::Postgres` | `Postgres.new(url:, pool_size: 5)` | Built-in connection pool |
52
+ | `Ask::State::Providers::MySQL` | `MySQL.new(url:)` | `utf8mb4` character set |
117
53
 
118
- ### MySQL (`Ask::State::Providers::MySQL`)
54
+ ## Adapter contract
119
55
 
120
- Best for teams already running MySQL or MariaDB.
56
+ Every backend implements `Ask::State::Adapter`:
121
57
 
122
- | Feature | Detail |
123
- |---------|--------|
124
- | **Driver** | [`mysql2`](https://github.com/brianmario/mysql2) |
125
- | **Configuration** | `MySQL.new(url:)` |
126
- | **Character set** | `utf8mb4` (full Unicode including emoji) |
58
+ - Key-value: `get`, `set(key, value, ttl:)`, `delete`, `set_if_not_exists`, `keys(pattern:)`, `clear`
59
+ - Distributed locking: `acquire_lock(key, ttl:)`, `release_lock(key, lock)`
60
+ - Message queues: `enqueue(queue, value)`, `dequeue(queue)`
61
+ - Ordered lists: `list_append(key, value, max_length:)`, `list_range(key, start, stop)`, `list_remove(key, value)`
127
62
 
128
- Uses prepared statements, `ON DUPLICATE KEY UPDATE`, and `SELECT ... LIMIT 1` for safe dequeue.
63
+ ## Full documentation
129
64
 
130
- ## API Reference
131
-
132
- All backends implement `Ask::State::Adapter`:
133
-
134
- ### Key-Value
135
-
136
- | Method | Description |
137
- |--------|-------------|
138
- | `get(key)` | Retrieve a value, or `nil` if missing or expired |
139
- | `set(key, value, ttl:)` | Store a value (JSON-serializable). `ttl` in seconds |
140
- | `delete(key)` | Remove a key |
141
- | `set_if_not_exists(key, value, ttl:)` | Create only if key doesn't exist (or is expired). Returns `true`/`false` |
142
- | `clear` | Remove all keys |
143
-
144
- ### Distributed Locking
145
-
146
- | Method | Description |
147
- |--------|-------------|
148
- | `acquire_lock(key, ttl:)` | Acquire a lock. Returns `Lock` or `nil` |
149
- | `release_lock(key, lock)` | Release a lock (only the owner can). Returns `true`/`false` |
150
-
151
- ### Message Queues
152
-
153
- | Method | Description |
154
- |--------|-------------|
155
- | `enqueue(queue, value)` | Push to the back of a queue. Returns `QueueEntry` |
156
- | `dequeue(queue)` | Pop from the front of a queue. Returns `QueueEntry` or `nil` |
157
- | `queue_depth(queue)` | Number of items in the queue |
158
-
159
- ### Ordered Lists
160
-
161
- | Method | Description |
162
- |--------|-------------|
163
- | `list_append(key, value, max_length:)` | Append to list. Trims to `max_length` (keeps newest) |
164
- | `list_range(key, start, stop)` | Slice of the list. `stop = -1` means all |
165
- | `list_remove(key, value)` | Remove all occurrences. Returns count removed |
166
-
167
- ### Lifecycle
168
-
169
- | Method | Description |
170
- |--------|-------------|
171
- | `close` | Close the connection(s) |
172
-
173
- ## Using with ask-agent Sessions
174
-
175
- ```ruby
176
- require "ask-state-providers"
177
- require "ask-agent"
178
-
179
- store = Ask::State::Providers::SQLite.new
180
-
181
- session = Ask::Agent::Session.new(
182
- "triage",
183
- model: "gpt-4o",
184
- state: store
185
- )
186
-
187
- session.run("What happened last time we saw this error?")
188
- # Every turn is persisted — survive restarts, searchable, auditable
189
- ```
65
+ The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs. [ask-state-providers in depth](https://ask-rb.github.io/ask-docs/reference/api#ask-state-providers) covers the adapter contract and backends. API reference: https://ask-rb.github.io/ask-docs/reference/api.
190
66
 
191
67
  ## Development
192
68
 
193
- ```bash
194
- # Install dependencies
69
+ ```
195
70
  bundle install
196
-
197
- # Run tests (SQLite tests run everywhere, Redis needs fakeredis,
198
- # Postgres/MySQL need DATABASE_URL/MYSQL_URL env vars)
199
71
  bundle exec rake test
200
-
201
- # Run tests with verbose output
202
- bundle exec ruby -Itest test/ask/state/providers/sqlite_test.rb
203
- bundle exec ruby -Itest test/ask/state/providers/redis_test.rb
204
-
205
- # Test Postgres locally
206
- DATABASE_URL="postgres://localhost:5432/ask_state_test" bundle exec rake test
207
-
208
- # Test MySQL locally
209
- MYSQL_URL="mysql2://root@localhost:3306/ask_state_test" bundle exec rake test
210
72
  ```
211
73
 
212
74
  ## License
213
75
 
214
- MIT — see [LICENSE](LICENSE).
215
-
216
- ## Links
217
-
218
- - **Source:** https://github.com/ask-rb/ask-state-providers
219
- - **Issues:** https://github.com/ask-rb/ask-state-providers/issues
220
- - **Docs:** https://github.com/ask-rb/ask-docs
76
+ MIT
@@ -41,7 +41,13 @@ module Ask
41
41
  end
42
42
 
43
43
  def delete(key)
44
- @mutex.synchronize { @data.delete(key) }
44
+ @mutex.synchronize do
45
+ @data.delete(key)
46
+ # delete removes everything under the key, including ordered
47
+ # lists (consumers store event feeds and queues as lists).
48
+ @lists.delete(key)
49
+ @queues.delete(key)
50
+ end
45
51
  end
46
52
 
47
53
  def set_if_not_exists(key, value, ttl: nil)
@@ -92,6 +92,9 @@ module Ask
92
92
 
93
93
  def delete(key)
94
94
  @client.prepare("DELETE FROM state_store WHERE `key` = ?").execute(key)
95
+ # delete removes everything under the key, including ordered
96
+ # lists (consumers store event feeds as lists).
97
+ @client.prepare("DELETE FROM lists WHERE list_key = ?").execute(key)
95
98
  end
96
99
 
97
100
  def set_if_not_exists(key, value, ttl: nil)
@@ -94,6 +94,9 @@ module Ask
94
94
  def delete(key)
95
95
  @pool.with do |conn|
96
96
  conn.exec_params("DELETE FROM state_store WHERE key = $1", [key])
97
+ # delete removes everything under the key, including ordered
98
+ # lists (consumers store event feeds as lists).
99
+ conn.exec_params("DELETE FROM lists WHERE list_key = $1", [key])
97
100
  end
98
101
  end
99
102
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "securerandom"
5
+ require "time"
5
6
 
6
7
  module Ask
7
8
  module State
@@ -44,7 +45,9 @@ module Ask
44
45
  end
45
46
 
46
47
  def delete(key)
47
- @redis.call("DEL", prefixed(key))
48
+ # delete removes everything under the key, including ordered
49
+ # lists (consumers store event feeds as lists).
50
+ @redis.call("DEL", prefixed(key), prefixed("list:#{key}"))
48
51
  end
49
52
 
50
53
  def set_if_not_exists(key, value, ttl: nil)
@@ -68,6 +68,9 @@ module Ask
68
68
  def delete(key)
69
69
  @mutex.synchronize do
70
70
  @db.execute("DELETE FROM state_store WHERE key = ?", [key])
71
+ # delete removes everything under the key, including ordered
72
+ # lists (consumers store event feeds as lists).
73
+ @db.execute("DELETE FROM lists WHERE list_key = ?", [key])
71
74
  end
72
75
  end
73
76
 
@@ -3,7 +3,7 @@
3
3
  module Ask
4
4
  module State
5
5
  module Providers
6
- VERSION = "0.4.0"
6
+ VERSION = "0.4.1"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-state-providers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto