ask-core 0.7.0 → 0.8.0
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/lib/ask/state.rb +2 -202
- data/lib/ask/stream.rb +1 -1
- data/lib/ask/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a806c5e8835fe74ef4bf8941a314ed462fa39bd3d607e6de112b92df9a8bbb8
|
|
4
|
+
data.tar.gz: a6301f23b50af2d5e508eb73e072951a2e3526a3c0c2776a92b24b281111ef93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 685fd487472dcb656e7b928e32d8e972ce27b963e78c8a64b167fe3a76691268878b87cf3079ff39d530cd23082d33ad919e36b01d209a790a9225c89d26b3bf
|
|
7
|
+
data.tar.gz: ba3bae4f9839de021ac62c3aefb0e9675b159151e5d45c5b113e48c6ec042594c658568944e78075b0c38e9dc945c02be13c40dfd2cf6901ed181b7bf9184fe3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [0.8.0] — 2026-07-28
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **`Ask::State::Memory` moved to `ask-state-providers`** — `ask-core` now only contains the abstract `State::Adapter` contract. The in-memory implementation is available in the `ask-state-providers` gem under the same class name (`Ask::State::Memory`). This keeps `ask-core` purely foundational.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Before (ask-core 0.7.x)
|
|
9
|
+
require "ask"
|
|
10
|
+
store = Ask::State::Memory.new
|
|
11
|
+
|
|
12
|
+
# After — add ask-state-providers gem
|
|
13
|
+
require "ask-state-providers"
|
|
14
|
+
store = Ask::State::Memory.new # same class name, same API
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Tested
|
|
18
|
+
|
|
19
|
+
- 281 tests, 549 assertions, 0 failures
|
|
20
|
+
|
|
1
21
|
## [0.7.0] — 2026-07-26
|
|
2
22
|
|
|
3
23
|
### Added
|
data/lib/ask/state.rb
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "securerandom"
|
|
4
|
-
require "forwardable"
|
|
5
|
-
|
|
6
3
|
module Ask
|
|
7
4
|
# Pluggable state backend for agent sessions, channel providers, and
|
|
8
5
|
# any other ask-rb component that needs durable key-value storage,
|
|
9
6
|
# distributed locking, message queues, or ordered lists.
|
|
10
7
|
#
|
|
11
8
|
# The {State::Adapter} abstract base defines the contract.
|
|
12
|
-
# {State::Memory} provides an in-process implementation backed by Hash.
|
|
13
9
|
# Production apps can provide Redis, PostgreSQL, or other backends by
|
|
14
10
|
# subclassing {State::Adapter}.
|
|
11
|
+
# {State::Memory} (in-process, Hash-backed) is available in the
|
|
12
|
+
# +ask-state-providers+ gem.
|
|
15
13
|
#
|
|
16
14
|
# @example Using the in-memory adapter
|
|
17
15
|
# store = Ask::State::Memory.new
|
|
@@ -187,203 +185,5 @@ module Ask
|
|
|
187
185
|
end
|
|
188
186
|
end
|
|
189
187
|
|
|
190
|
-
# In-process state backend backed by a Hash.
|
|
191
|
-
# All operations are thread-safe via a Mutex.
|
|
192
|
-
# Data is not persisted — lost on process exit.
|
|
193
|
-
class Memory < Adapter
|
|
194
|
-
def initialize
|
|
195
|
-
@data = {}
|
|
196
|
-
@locks = {}
|
|
197
|
-
@queues = {}
|
|
198
|
-
@lists = {}
|
|
199
|
-
@mutex = Mutex.new
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# -- key-value --
|
|
203
|
-
|
|
204
|
-
def get(key)
|
|
205
|
-
@mutex.synchronize do
|
|
206
|
-
expiry = @data[key]&.dig(:expires_at)
|
|
207
|
-
return nil if expiry && Time.now >= expiry
|
|
208
|
-
|
|
209
|
-
@data[key]&.dig(:value)
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def set(key, value, ttl: nil)
|
|
214
|
-
@mutex.synchronize do
|
|
215
|
-
@data[key] = {
|
|
216
|
-
value: value,
|
|
217
|
-
expires_at: ttl ? Time.now + ttl : nil
|
|
218
|
-
}
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def delete(key)
|
|
223
|
-
@mutex.synchronize { @data.delete(key) }
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def set_if_not_exists(key, value, ttl: nil)
|
|
227
|
-
@mutex.synchronize do
|
|
228
|
-
if @data.key?(key)
|
|
229
|
-
expiry = @data[key][:expires_at]
|
|
230
|
-
return false if expiry.nil? || Time.now < expiry
|
|
231
|
-
|
|
232
|
-
# Key expired — treat as nonexistent
|
|
233
|
-
@data.delete(key)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
@data[key] = {
|
|
237
|
-
value: value,
|
|
238
|
-
expires_at: ttl ? Time.now + ttl : nil
|
|
239
|
-
}
|
|
240
|
-
true
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def clear
|
|
245
|
-
@mutex.synchronize do
|
|
246
|
-
@data.clear
|
|
247
|
-
@locks.clear
|
|
248
|
-
@queues.clear
|
|
249
|
-
@lists.clear
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def exists?(key)
|
|
254
|
-
@mutex.synchronize do
|
|
255
|
-
expiry = @data[key]&.dig(:expires_at)
|
|
256
|
-
return false if expiry && Time.now >= expiry
|
|
257
|
-
@data.key?(key)
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def keys(pattern: nil)
|
|
262
|
-
@mutex.synchronize do
|
|
263
|
-
# Filter out expired keys
|
|
264
|
-
active = @data.select do |_k, v|
|
|
265
|
-
expiry = v[:expires_at]
|
|
266
|
-
expiry.nil? || Time.now < expiry
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
keys = active.keys.map(&:to_s)
|
|
270
|
-
return keys unless pattern
|
|
271
|
-
|
|
272
|
-
regex = self.class.glob_to_regex(pattern)
|
|
273
|
-
keys.select { |k| k.match?(regex) }
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# -- locking --
|
|
278
|
-
|
|
279
|
-
def acquire_lock(key, ttl: 10)
|
|
280
|
-
@mutex.synchronize do
|
|
281
|
-
existing = @locks[key]
|
|
282
|
-
if existing.nil? || existing.expired?
|
|
283
|
-
lock = Lock.new(
|
|
284
|
-
id: key,
|
|
285
|
-
token: SecureRandom.hex(16),
|
|
286
|
-
expires_at: Time.now + ttl
|
|
287
|
-
)
|
|
288
|
-
@locks[key] = lock
|
|
289
|
-
lock
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def release_lock(key, lock)
|
|
295
|
-
@mutex.synchronize do
|
|
296
|
-
current = @locks[key]
|
|
297
|
-
if current && current.token == lock.token && !current.expired?
|
|
298
|
-
@locks.delete(key)
|
|
299
|
-
true
|
|
300
|
-
else
|
|
301
|
-
false
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
# -- queues --
|
|
307
|
-
|
|
308
|
-
def enqueue(queue, value)
|
|
309
|
-
@mutex.synchronize do
|
|
310
|
-
@queues[queue] ||= []
|
|
311
|
-
entry = QueueEntry.new(
|
|
312
|
-
id: SecureRandom.uuid,
|
|
313
|
-
value: value,
|
|
314
|
-
enqueued_at: Time.now
|
|
315
|
-
)
|
|
316
|
-
@queues[queue] << entry
|
|
317
|
-
entry
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def dequeue(queue)
|
|
322
|
-
@mutex.synchronize do
|
|
323
|
-
q = @queues[queue]
|
|
324
|
-
return nil unless q&.any?
|
|
325
|
-
|
|
326
|
-
q.shift
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def queue_depth(queue)
|
|
331
|
-
@mutex.synchronize do
|
|
332
|
-
(@queues[queue] || []).length
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# -- lists --
|
|
337
|
-
|
|
338
|
-
def list_append(key, value, max_length: nil)
|
|
339
|
-
@mutex.synchronize do
|
|
340
|
-
@lists[key] ||= []
|
|
341
|
-
@lists[key] << value
|
|
342
|
-
@lists[key].shift if max_length && @lists[key].length > max_length
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def list_range(key, start = 0, stop = -1)
|
|
347
|
-
@mutex.synchronize do
|
|
348
|
-
list = @lists[key] || []
|
|
349
|
-
return list if start == 0 && stop == -1
|
|
350
|
-
|
|
351
|
-
stop = list.length - 1 if stop == -1 || stop >= list.length
|
|
352
|
-
return [] if start > stop
|
|
353
|
-
|
|
354
|
-
list[start..stop] || []
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
def list_remove(key, value)
|
|
359
|
-
@mutex.synchronize do
|
|
360
|
-
list = @lists[key]
|
|
361
|
-
return 0 unless list
|
|
362
|
-
|
|
363
|
-
before = list.length
|
|
364
|
-
list.delete(value)
|
|
365
|
-
before - list.length
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# -- lifecycle --
|
|
370
|
-
|
|
371
|
-
def close
|
|
372
|
-
@mutex.synchronize do
|
|
373
|
-
@data.clear
|
|
374
|
-
@locks.clear
|
|
375
|
-
@queues.clear
|
|
376
|
-
@lists.clear
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
private
|
|
381
|
-
|
|
382
|
-
# Convert a glob pattern (*, ?) to a Regexp.
|
|
383
|
-
# Delegates to the shared class method for consistency.
|
|
384
|
-
def glob_to_regex(pattern)
|
|
385
|
-
self.class.glob_to_regex(pattern)
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
188
|
end
|
|
389
189
|
end
|
data/lib/ask/stream.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Ask
|
|
|
35
35
|
def finished? = !@finish_reason.nil?
|
|
36
36
|
|
|
37
37
|
# @return [Boolean] true if this chunk contains tool calls
|
|
38
|
-
def tool_call? = @tool_calls
|
|
38
|
+
def tool_call? = @tool_calls.respond_to?(:any?) && @tool_calls.any?
|
|
39
39
|
|
|
40
40
|
# @return [Boolean] true if this chunk contains thinking/reasoning content
|
|
41
41
|
def thinking? = @thinking.to_s.length > 0
|
data/lib/ask/version.rb
CHANGED