ask-core 0.2.5 → 0.4.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 +65 -0
- data/lib/ask/provider_tool.rb +95 -0
- data/lib/ask/state.rb +310 -0
- data/lib/ask/version.rb +1 -1
- data/lib/ask.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76460c5c6e631cb59ed89bb8fabb48cd52dca8d6930c99e43ec34b8f4b9a0912
|
|
4
|
+
data.tar.gz: cbd34d8e2dec789316f4b7e7f1aead500dc80983c003fd6a8ef657dfa9c7a299
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bea59c9b587c6c3f00a2aa4ce4b9052e00af5d4f924eccc395fce73560a99cf7fafa586e813d7b11efbdd74a39e20dc85ca2c6627c49be63256e8485fdf5be16
|
|
7
|
+
data.tar.gz: feb067f7cc3f11bda5f942614df3717da8e06ceada1a4ad2c92fe07a8c05b613fd240cab929217bb7cebc4f273e7e5e5d7d338ecaefd579b92d3ac3bb61c6b3f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
+
## [0.4.0] — 2026-07-21
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Provider-executed tools** — `Ask::ProviderTool` value objects for configuring built-in tools that run on the LLM provider's infrastructure (web search, file search, code execution). Supports factory methods for OpenAI's built-in tools.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Ask::ProviderTool.web_search(search_context_size: "high")
|
|
9
|
+
Ask::ProviderTool.file_search(vector_store_ids: ["vs_abc"], max_num_results: 10)
|
|
10
|
+
Ask::ProviderTool.code_interpreter(file_ids: ["file_1"])
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Provider tools are identified by a fully qualified `id` (e.g. `"openai.web_search"`) and carry provider-specific `args`. They are `frozen` value objects with equality based on `id` + `args`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **ask-llm-providers OpenAI** — `chat` now splits provider tools from regular tools. When provider tools are present, the Responses API is used instead of Chat Completions. Regular function tools continue to use the existing Chat Completions path.
|
|
18
|
+
- **ask-agent Loop** — `ResponseMessage` now carries `tool_results` for pre-computed provider-executed results. The loop adds them directly to the conversation without local execution, then continues with any remaining user tool calls.
|
|
19
|
+
- **ResponseMessage** — `tool_results` field added with default `{}`. All existing call sites are backward compatible via the custom `initialize` with keyword defaults.
|
|
20
|
+
|
|
21
|
+
### Tested
|
|
22
|
+
|
|
23
|
+
- 13 new tests for `Ask::ProviderTool`: creation, factory methods, args, frozen, equality, flags.
|
|
24
|
+
- 13 new integration tests for provider-executed tools: loop handling with mixed tools, only provider tools, tool splitting in OpenAI provider, Responses API tool formatting.
|
|
25
|
+
- Full test suite: 248 ask-core tests, 329 ask-agent tests — 0 failures.
|
|
26
|
+
|
|
27
|
+
## [0.3.0] — 2026-07-21
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Pluggable state abstraction** — `Ask::State::Adapter` defines a unified interface for key-value storage, distributed locking, message queues, and ordered lists. `Ask::State::Memory` provides an in-process, thread-safe implementation backed by Hash.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
store = Ask::State::Memory.new
|
|
35
|
+
|
|
36
|
+
# Key-value with optional TTL
|
|
37
|
+
store.set("key", "value", ttl: 60)
|
|
38
|
+
store.get("key")
|
|
39
|
+
|
|
40
|
+
# Distributed locking
|
|
41
|
+
lock = store.acquire_lock("resource", ttl: 10)
|
|
42
|
+
store.release_lock("resource", lock)
|
|
43
|
+
|
|
44
|
+
# Message queues
|
|
45
|
+
store.enqueue("queue", { task: "check" })
|
|
46
|
+
store.dequeue("queue")
|
|
47
|
+
|
|
48
|
+
# Ordered lists
|
|
49
|
+
store.list_append("list", "item", max_length: 100)
|
|
50
|
+
store.list_range("list", 0, -1)
|
|
51
|
+
store.list_remove("list", "item")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The adapter pattern mirrors `Ask::Provider` — define the contract in ask-core, provide implementations in separate gems. Production backends (Redis, PostgreSQL) can be added by any gem without modifying ask-core.
|
|
55
|
+
|
|
56
|
+
Data types: `Ask::State::Lock` (with `#expired?`), `Ask::State::QueueEntry`.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **ask-agent's `Persistence::Base` now wraps `Ask::State::Adapter`** — session persistence is backed by the unified state interface instead of a standalone abstract class. `Persistence::InMemory` delegates to `Ask::State::Memory`. Backward compatible — no API changes for users.
|
|
61
|
+
|
|
62
|
+
### Tested
|
|
63
|
+
|
|
64
|
+
- 34 new tests for `Ask::State::Adapter` + `Ask::State::Memory`: key-value operations, TTL expiry, thread safety, locking semantics, queue FIFO order, list management, adapter subclassing, and the base class contract.
|
|
65
|
+
|
|
1
66
|
## [0.2.4] — 2026-07-17
|
|
2
67
|
|
|
3
68
|
### Added
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
# Configuration for a provider-defined or provider-executed tool.
|
|
5
|
+
#
|
|
6
|
+
# Provider tools are built-in capabilities that the LLM provider offers
|
|
7
|
+
# natively — web search, file search, code execution, image generation,
|
|
8
|
+
# and so on. They are not implemented by user code but by the provider
|
|
9
|
+
# itself.
|
|
10
|
+
#
|
|
11
|
+
# @example Configuring a provider-executed web search
|
|
12
|
+
# Ask::ProviderTool.new(
|
|
13
|
+
# id: "openai.web_search",
|
|
14
|
+
# name: "web_search",
|
|
15
|
+
# description: "Search the internet",
|
|
16
|
+
# args: { search_context_size: "medium" }
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# @example Using shorthand factory methods
|
|
20
|
+
# Ask::ProviderTool.web_search(search_context_size: "high")
|
|
21
|
+
# Ask::ProviderTool.file_search(max_num_results: 10)
|
|
22
|
+
class ProviderTool
|
|
23
|
+
# @return [String] fully qualified tool identifier (e.g. "openai.web_search")
|
|
24
|
+
attr_reader :id
|
|
25
|
+
|
|
26
|
+
# @return [String] short tool name
|
|
27
|
+
attr_reader :name
|
|
28
|
+
|
|
29
|
+
# @return [String] human-readable description
|
|
30
|
+
attr_reader :description
|
|
31
|
+
|
|
32
|
+
# @return [Hash] provider-specific configuration arguments
|
|
33
|
+
attr_reader :args
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] true if the provider handles execution on its side
|
|
36
|
+
def provider_executed?
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] true — marks this as a provider tool for routing
|
|
41
|
+
def provider_tool?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(id:, name:, description: "", args: {})
|
|
46
|
+
@id = id
|
|
47
|
+
@name = name
|
|
48
|
+
@description = description
|
|
49
|
+
@args = args.dup.freeze
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# OpenAI web search tool.
|
|
55
|
+
# @param search_context_size [String, nil] "low", "medium", or "high"
|
|
56
|
+
# @param user_location [Hash, nil] approximate location { type: "approximate", country: "US", ... }
|
|
57
|
+
def web_search(search_context_size: nil, user_location: nil, allowed_domains: nil)
|
|
58
|
+
args = {}.tap do |h|
|
|
59
|
+
h[:search_context_size] = search_context_size if search_context_size
|
|
60
|
+
h[:user_location] = user_location if user_location
|
|
61
|
+
h[:allowed_domains] = allowed_domains if allowed_domains
|
|
62
|
+
end
|
|
63
|
+
new(id: "openai.web_search", name: "web_search", description: "Search the internet for current information", args: args)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# OpenAI file search tool. Requires a vector store.
|
|
67
|
+
# @param vector_store_ids [Array<String>] IDs of vector stores to search
|
|
68
|
+
# @param max_num_results [Integer, nil] maximum number of results
|
|
69
|
+
def file_search(vector_store_ids:, max_num_results: nil)
|
|
70
|
+
args = { vector_store_ids: vector_store_ids }.tap do |h|
|
|
71
|
+
h[:max_num_results] = max_num_results if max_num_results
|
|
72
|
+
end
|
|
73
|
+
new(id: "openai.file_search", name: "file_search", description: "Search through uploaded files", args: args)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# OpenAI code interpreter tool.
|
|
77
|
+
# @param file_ids [Array<String>, nil] IDs of files to make available
|
|
78
|
+
def code_interpreter(file_ids: nil)
|
|
79
|
+
args = file_ids ? { file_ids: file_ids } : {}
|
|
80
|
+
new(id: "openai.code_interpreter", name: "code_interpreter", description: "Execute Python code in a sandboxed environment", args: args)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Equality based on id + args.
|
|
85
|
+
def ==(other)
|
|
86
|
+
return false unless other.is_a?(ProviderTool)
|
|
87
|
+
id == other.id && args == other.args
|
|
88
|
+
end
|
|
89
|
+
alias eql? ==
|
|
90
|
+
|
|
91
|
+
def hash
|
|
92
|
+
[id, args].hash
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/ask/state.rb
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
|
|
6
|
+
module Ask
|
|
7
|
+
# Pluggable state backend for agent sessions, channel providers, and
|
|
8
|
+
# any other ask-rb component that needs durable key-value storage,
|
|
9
|
+
# distributed locking, message queues, or ordered lists.
|
|
10
|
+
#
|
|
11
|
+
# The {State::Adapter} abstract base defines the contract.
|
|
12
|
+
# {State::Memory} provides an in-process implementation backed by Hash.
|
|
13
|
+
# Production apps can provide Redis, PostgreSQL, or other backends by
|
|
14
|
+
# subclassing {State::Adapter}.
|
|
15
|
+
#
|
|
16
|
+
# @example Using the in-memory adapter
|
|
17
|
+
# store = Ask::State::Memory.new
|
|
18
|
+
# store.set("key", "value")
|
|
19
|
+
# store.get("key") # => "value"
|
|
20
|
+
# store.delete("key")
|
|
21
|
+
#
|
|
22
|
+
# @example Acquiring a distributed lock
|
|
23
|
+
# lock = store.acquire_lock("resource-1", ttl: 10)
|
|
24
|
+
# if lock
|
|
25
|
+
# begin
|
|
26
|
+
# # critical section
|
|
27
|
+
# ensure
|
|
28
|
+
# store.release_lock("resource-1", lock)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# @example Using a message queue
|
|
33
|
+
# store.enqueue("queue-name", { task: "check_health" })
|
|
34
|
+
# entry = store.dequeue("queue-name")
|
|
35
|
+
# # => { id: "uuid", value: { task: "check_health" }, enqueued_at: timestamp }
|
|
36
|
+
module State
|
|
37
|
+
Lock = Data.define(:id, :token, :expires_at) do
|
|
38
|
+
def expired?(now = Time.now)
|
|
39
|
+
expires_at && now >= expires_at
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
QueueEntry = Data.define(:id, :value, :enqueued_at)
|
|
44
|
+
|
|
45
|
+
# Abstract base class for state backends.
|
|
46
|
+
# Subclasses must implement all methods.
|
|
47
|
+
class Adapter
|
|
48
|
+
# Key-value storage
|
|
49
|
+
|
|
50
|
+
# @param key [String] the key
|
|
51
|
+
# @return [Object, nil] the stored value, or nil if not found
|
|
52
|
+
def get(key)
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param key [String] the key
|
|
57
|
+
# @param value [Object] the value (must be JSON-serializable)
|
|
58
|
+
# @param ttl [Integer, nil] time-to-live in seconds (nil = no expiry)
|
|
59
|
+
def set(key, value, ttl: nil)
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param key [String] the key
|
|
64
|
+
def delete(key)
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Atomically set a value only if the key does not already exist.
|
|
69
|
+
# @param key [String] the key
|
|
70
|
+
# @param value [Object] the value
|
|
71
|
+
# @return [Boolean] true if the value was set, false if the key already exists
|
|
72
|
+
def set_if_not_exists(key, value, ttl: nil)
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Distributed locking
|
|
77
|
+
|
|
78
|
+
# Acquire a lock for a key. Returns nil if the lock is held by another owner.
|
|
79
|
+
# @param key [String] the resource to lock
|
|
80
|
+
# @param ttl [Integer] lock time-to-live in seconds (default 10)
|
|
81
|
+
# @return [Lock, nil] the lock if acquired, nil if already held
|
|
82
|
+
def acquire_lock(key, ttl: 10)
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Release a lock. Only the lock owner can release it.
|
|
87
|
+
# @param key [String] the resource to unlock
|
|
88
|
+
# @param lock [Lock] the lock returned by {#acquire_lock}
|
|
89
|
+
# @return [Boolean] true if released, false if lock was already expired or not held
|
|
90
|
+
def release_lock(key, lock)
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Message queues
|
|
95
|
+
|
|
96
|
+
# Push an item onto a named queue.
|
|
97
|
+
# @param queue [String] the queue name
|
|
98
|
+
# @param value [Object] the value to enqueue
|
|
99
|
+
# @return [QueueEntry] the enqueued entry
|
|
100
|
+
def enqueue(queue, value)
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Pop the next item from a named queue.
|
|
105
|
+
# @param queue [String] the queue name
|
|
106
|
+
# @return [QueueEntry, nil] the next entry, or nil if the queue is empty
|
|
107
|
+
def dequeue(queue)
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param queue [String] the queue name
|
|
112
|
+
# @return [Integer] the number of items in the queue
|
|
113
|
+
def queue_depth(queue)
|
|
114
|
+
raise NotImplementedError
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Ordered lists
|
|
118
|
+
|
|
119
|
+
# Append a value to an ordered list. Trims to max_length (keeps newest).
|
|
120
|
+
# @param key [String] the list key
|
|
121
|
+
# @param value [Object] the value to append
|
|
122
|
+
# @param max_length [Integer, nil] maximum list length (nil = no limit)
|
|
123
|
+
def list_append(key, value, max_length: nil)
|
|
124
|
+
raise NotImplementedError
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Return a slice of the list.
|
|
128
|
+
# @param key [String] the list key
|
|
129
|
+
# @param start [Integer] starting index (0-based)
|
|
130
|
+
# @param stop [Integer] ending index (inclusive, -1 for all)
|
|
131
|
+
# @return [Array<Object>] the list slice
|
|
132
|
+
def list_range(key, start = 0, stop = -1)
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Remove all occurrences of a value from a list.
|
|
137
|
+
# @param key [String] the list key
|
|
138
|
+
# @param value [Object] the value to remove
|
|
139
|
+
# @return [Integer] number of removed elements
|
|
140
|
+
def list_remove(key, value)
|
|
141
|
+
raise NotImplementedError
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Lifecycle
|
|
145
|
+
|
|
146
|
+
# Optional: called when the adapter is no longer needed.
|
|
147
|
+
def close
|
|
148
|
+
# no-op by default
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# In-process state backend backed by a Hash.
|
|
153
|
+
# All operations are thread-safe via a Mutex.
|
|
154
|
+
# Data is not persisted — lost on process exit.
|
|
155
|
+
class Memory < Adapter
|
|
156
|
+
def initialize
|
|
157
|
+
@data = {}
|
|
158
|
+
@locks = {}
|
|
159
|
+
@queues = {}
|
|
160
|
+
@lists = {}
|
|
161
|
+
@mutex = Mutex.new
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# -- key-value --
|
|
165
|
+
|
|
166
|
+
def get(key)
|
|
167
|
+
@mutex.synchronize do
|
|
168
|
+
expiry = @data[key]&.dig(:expires_at)
|
|
169
|
+
return nil if expiry && Time.now >= expiry
|
|
170
|
+
|
|
171
|
+
@data[key]&.dig(:value)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def set(key, value, ttl: nil)
|
|
176
|
+
@mutex.synchronize do
|
|
177
|
+
@data[key] = {
|
|
178
|
+
value: value,
|
|
179
|
+
expires_at: ttl ? Time.now + ttl : nil
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def delete(key)
|
|
185
|
+
@mutex.synchronize { @data.delete(key) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def set_if_not_exists(key, value, ttl: nil)
|
|
189
|
+
@mutex.synchronize do
|
|
190
|
+
if @data.key?(key)
|
|
191
|
+
expiry = @data[key][:expires_at]
|
|
192
|
+
return false if expiry.nil? || Time.now < expiry
|
|
193
|
+
|
|
194
|
+
# Key expired — treat as nonexistent
|
|
195
|
+
@data.delete(key)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
@data[key] = {
|
|
199
|
+
value: value,
|
|
200
|
+
expires_at: ttl ? Time.now + ttl : nil
|
|
201
|
+
}
|
|
202
|
+
true
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# -- locking --
|
|
207
|
+
|
|
208
|
+
def acquire_lock(key, ttl: 10)
|
|
209
|
+
@mutex.synchronize do
|
|
210
|
+
existing = @locks[key]
|
|
211
|
+
if existing.nil? || existing.expired?
|
|
212
|
+
lock = Lock.new(
|
|
213
|
+
id: key,
|
|
214
|
+
token: SecureRandom.hex(16),
|
|
215
|
+
expires_at: Time.now + ttl
|
|
216
|
+
)
|
|
217
|
+
@locks[key] = lock
|
|
218
|
+
lock
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def release_lock(key, lock)
|
|
224
|
+
@mutex.synchronize do
|
|
225
|
+
current = @locks[key]
|
|
226
|
+
if current && current.token == lock.token && !current.expired?
|
|
227
|
+
@locks.delete(key)
|
|
228
|
+
true
|
|
229
|
+
else
|
|
230
|
+
false
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# -- queues --
|
|
236
|
+
|
|
237
|
+
def enqueue(queue, value)
|
|
238
|
+
@mutex.synchronize do
|
|
239
|
+
@queues[queue] ||= []
|
|
240
|
+
entry = QueueEntry.new(
|
|
241
|
+
id: SecureRandom.uuid,
|
|
242
|
+
value: value,
|
|
243
|
+
enqueued_at: Time.now
|
|
244
|
+
)
|
|
245
|
+
@queues[queue] << entry
|
|
246
|
+
entry
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def dequeue(queue)
|
|
251
|
+
@mutex.synchronize do
|
|
252
|
+
q = @queues[queue]
|
|
253
|
+
return nil unless q&.any?
|
|
254
|
+
|
|
255
|
+
q.shift
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def queue_depth(queue)
|
|
260
|
+
@mutex.synchronize do
|
|
261
|
+
(@queues[queue] || []).length
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# -- lists --
|
|
266
|
+
|
|
267
|
+
def list_append(key, value, max_length: nil)
|
|
268
|
+
@mutex.synchronize do
|
|
269
|
+
@lists[key] ||= []
|
|
270
|
+
@lists[key] << value
|
|
271
|
+
@lists[key].shift if max_length && @lists[key].length > max_length
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def list_range(key, start = 0, stop = -1)
|
|
276
|
+
@mutex.synchronize do
|
|
277
|
+
list = @lists[key] || []
|
|
278
|
+
return list if start == 0 && stop == -1
|
|
279
|
+
|
|
280
|
+
stop = list.length - 1 if stop == -1 || stop >= list.length
|
|
281
|
+
return [] if start > stop
|
|
282
|
+
|
|
283
|
+
list[start..stop] || []
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def list_remove(key, value)
|
|
288
|
+
@mutex.synchronize do
|
|
289
|
+
list = @lists[key]
|
|
290
|
+
return 0 unless list
|
|
291
|
+
|
|
292
|
+
before = list.length
|
|
293
|
+
list.delete(value)
|
|
294
|
+
before - list.length
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# -- lifecycle --
|
|
299
|
+
|
|
300
|
+
def close
|
|
301
|
+
@mutex.synchronize do
|
|
302
|
+
@data.clear
|
|
303
|
+
@locks.clear
|
|
304
|
+
@queues.clear
|
|
305
|
+
@lists.clear
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
data/lib/ask/version.rb
CHANGED
data/lib/ask.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -68,7 +68,9 @@ files:
|
|
|
68
68
|
- lib/ask/errors.rb
|
|
69
69
|
- lib/ask/models.rb
|
|
70
70
|
- lib/ask/provider.rb
|
|
71
|
+
- lib/ask/provider_tool.rb
|
|
71
72
|
- lib/ask/result.rb
|
|
73
|
+
- lib/ask/state.rb
|
|
72
74
|
- lib/ask/stream.rb
|
|
73
75
|
- lib/ask/tool_def.rb
|
|
74
76
|
- lib/ask/version.rb
|