ask-agent 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +42 -0
- data/lib/ask/agent/configuration.rb +6 -0
- data/lib/ask/agent/persistence/base.rb +50 -4
- data/lib/ask/agent/persistence/in_memory.rb +3 -18
- data/lib/ask/agent/scheduler.rb +172 -0
- data/lib/ask/agent/version.rb +1 -1
- data/lib/ask/agent.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55ad6219a4374a7aeb4688533bcf7a0a68ebb166de2d96d2e9953eb6814fe5c5
|
|
4
|
+
data.tar.gz: 89e4e0413d96f90dd30d4203fa4023331663b2818a5df89c0a60a6975fb8bcb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9557223b85bc04778cd6821a9b7807818e06ac85124d95f79469db613a7a23d8e8317db5e6454517adf5b67fd68806eafc4ec145f8c3ea08d01de93d4f939a91
|
|
7
|
+
data.tar.gz: 53d3a056d6c8004bdd32536f3781bae6a0bee21bd12f1af12bc3eefab02e3f229df6109c8f5f2c6802229177aa28c27a17804bb348e93c4898adce62289222d5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## [0.6.1] — 2026-07-21
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **`Persistence::Base` now wraps `Ask::State::Adapter`** (from ask-core 0.3.0). Session persistence is backed by the unified state interface instead of a standalone abstract class. `Persistence::InMemory` delegates to `Ask::State::Memory`. The public API is unchanged — `save`, `load`, `delete`, and `list` work identically.
|
|
6
|
+
- **`Persistence::Base.new` accepts `state_adapter:` keyword** for custom backends. Defaults to `Ask::State::Memory` (same behavior as before).
|
|
7
|
+
- **`Persistence::Base#list`** now returns a deduplicated list ordered by most-recently-saved.
|
|
8
|
+
|
|
9
|
+
## [0.6.0] — 2026-07-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Agent Scheduler** — `Ask::Agent::Scheduler` runs recurring agent tasks on cron schedules or human-readable intervals. Configure tasks alongside middleware and transforms, then start the background loop.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Ask::Agent.configure do |c|
|
|
17
|
+
c.scheduler.every "5 minutes", name: "health-check" do
|
|
18
|
+
Ask::Agent::Session.new(model: "gpt-4o").run("Check server health")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
c.scheduler.cron "0 9 * * 1-5", name: "morning-report" do
|
|
22
|
+
Ask::Agent::Session.new(model: "gpt-4o").run("Generate daily report")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Ask::Agent::Scheduler.start # background thread loop
|
|
27
|
+
Ask::Agent::Scheduler.stop # graceful shutdown
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Manage the scheduler at runtime:
|
|
31
|
+
- `Ask::Agent::Scheduler.running?` — check if the loop is active
|
|
32
|
+
- `Ask::Agent::Scheduler.jobs` — list all scheduled jobs (returns `Rufus::Scheduler::Job` objects with `.name`, `.next_time`, etc.)
|
|
33
|
+
- `Ask::Agent::Scheduler.job_by_name("health-check")` — find a specific job
|
|
34
|
+
- Tasks without blocks are valid — they register but execute nothing
|
|
35
|
+
|
|
36
|
+
Powered by `rufus-scheduler` (added as a runtime dependency). The scheduler is optional — users who don't configure any tasks are unaffected.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`Ask::Agent::Configuration`** now exposes `#scheduler` returning a `SchedulerConfig` DSL proxy. No breaking changes for existing users.
|
|
41
|
+
- **Gemspec** — added `rufus-scheduler ~> 3.9` as a runtime dependency.
|
|
42
|
+
|
|
1
43
|
## [0.5.0] — 2026-07-21
|
|
2
44
|
|
|
3
45
|
### Added
|
|
@@ -22,6 +22,12 @@ module Ask
|
|
|
22
22
|
|
|
23
23
|
@middleware = Middleware::Pipeline.new
|
|
24
24
|
@stream_transforms = StreamTransforms::Pipeline.new
|
|
25
|
+
@scheduler_config = SchedulerConfig.new(self)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [SchedulerConfig] DSL for defining recurring agent tasks
|
|
29
|
+
def scheduler
|
|
30
|
+
@scheduler_config
|
|
25
31
|
end
|
|
26
32
|
end
|
|
27
33
|
end
|
|
@@ -3,21 +3,67 @@
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Agent
|
|
5
5
|
module Persistence
|
|
6
|
+
# Session persistence backed by a generic {Ask::State::Adapter}.
|
|
7
|
+
#
|
|
8
|
+
# This wraps session data under namespaced keys so multiple sessions
|
|
9
|
+
# can coexist in the same state backend alongside other data.
|
|
10
|
+
#
|
|
11
|
+
# @example With the default in-memory store
|
|
12
|
+
# store = Persistence::InMemory.new
|
|
13
|
+
# store.save("session-1", { messages: [...] })
|
|
14
|
+
# store.load("session-1")
|
|
15
|
+
#
|
|
16
|
+
# @example With a custom state adapter (e.g. Redis)
|
|
17
|
+
# store = Persistence::Base.new(
|
|
18
|
+
# state_adapter: MyRedisAdapter.new
|
|
19
|
+
# )
|
|
20
|
+
# store.save("session-1", data)
|
|
6
21
|
class Base
|
|
22
|
+
KEY_PREFIX = "ask:session:"
|
|
23
|
+
INDEX_KEY = "ask:session:index"
|
|
24
|
+
|
|
25
|
+
# @param state_adapter [Ask::State::Adapter] backend store (defaults to Memory)
|
|
26
|
+
def initialize(state_adapter: nil)
|
|
27
|
+
@state = state_adapter || Ask::State::Memory.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Ask::State::Adapter] the underlying state adapter
|
|
31
|
+
attr_reader :state
|
|
32
|
+
|
|
33
|
+
# Persist a session.
|
|
34
|
+
# @param session_id [String] the session identifier
|
|
35
|
+
# @param data [Hash] the session data
|
|
7
36
|
def save(session_id, data)
|
|
8
|
-
|
|
37
|
+
@state.set(key(session_id), data)
|
|
38
|
+
# Keep the index list unique — remove existing entry first then append
|
|
39
|
+
@state.list_remove(INDEX_KEY, session_id)
|
|
40
|
+
@state.list_append(INDEX_KEY, session_id)
|
|
9
41
|
end
|
|
10
42
|
|
|
43
|
+
# Load a session.
|
|
44
|
+
# @param session_id [String] the session identifier
|
|
45
|
+
# @return [Hash, nil] the session data, or nil if not found
|
|
11
46
|
def load(session_id)
|
|
12
|
-
|
|
47
|
+
@state.get(key(session_id))
|
|
13
48
|
end
|
|
14
49
|
|
|
50
|
+
# Delete a session.
|
|
51
|
+
# @param session_id [String] the session identifier
|
|
15
52
|
def delete(session_id)
|
|
16
|
-
|
|
53
|
+
@state.delete(key(session_id))
|
|
54
|
+
@state.list_remove(INDEX_KEY, session_id)
|
|
17
55
|
end
|
|
18
56
|
|
|
57
|
+
# List all stored session IDs.
|
|
58
|
+
# @return [Array<String>] list of session IDs
|
|
19
59
|
def list
|
|
20
|
-
|
|
60
|
+
@state.list_range(INDEX_KEY, 0, -1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def key(session_id)
|
|
66
|
+
"#{KEY_PREFIX}#{session_id}"
|
|
21
67
|
end
|
|
22
68
|
end
|
|
23
69
|
end
|
|
@@ -3,26 +3,11 @@
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Agent
|
|
5
5
|
module Persistence
|
|
6
|
+
# In-memory session persistence. Backed by {Ask::State::Memory}.
|
|
7
|
+
# Data is lost when the process exits.
|
|
6
8
|
class InMemory < Base
|
|
7
9
|
def initialize
|
|
8
|
-
|
|
9
|
-
@mutex = Mutex.new
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def save(session_id, data)
|
|
13
|
-
@mutex.synchronize { @store[session_id] = data }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def load(session_id)
|
|
17
|
-
@mutex.synchronize { @store[session_id] }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def delete(session_id)
|
|
21
|
-
@mutex.synchronize { @store.delete(session_id) }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def list
|
|
25
|
-
@mutex.synchronize { @store.keys }
|
|
10
|
+
super(state_adapter: Ask::State::Memory.new)
|
|
26
11
|
end
|
|
27
12
|
end
|
|
28
13
|
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Agent
|
|
5
|
+
# Schedules recurring agent runs.
|
|
6
|
+
#
|
|
7
|
+
# Configure tasks through {Ask::Agent::Configuration#scheduler}, then
|
|
8
|
+
# start the scheduler loop. Tasks run in background threads managed by
|
|
9
|
+
# +rufus-scheduler+.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Ask::Agent.configure do |c|
|
|
13
|
+
# c.scheduler.every "5 minutes", name: "health-check" do
|
|
14
|
+
# Ask::Agent::Session.new(model: "gpt-4o").run("Check server health")
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# c.scheduler.cron "0 9 * * 1-5", name: "morning-report" do
|
|
18
|
+
# Ask::Agent::Session.new(model: "gpt-4o").run("Generate daily report")
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Ask::Agent::Scheduler.start # background thread loop
|
|
23
|
+
# Ask::Agent::Scheduler.running? # => true
|
|
24
|
+
# Ask::Agent::Scheduler.jobs # => list of rufus jobs
|
|
25
|
+
# Ask::Agent::Scheduler.stop # graceful shutdown
|
|
26
|
+
class Scheduler
|
|
27
|
+
@instance = nil
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# @return [Scheduler] the singleton instance
|
|
32
|
+
def instance
|
|
33
|
+
@instance ||= new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Start the scheduler background thread.
|
|
37
|
+
# All configured tasks begin running on their schedules.
|
|
38
|
+
# @return [Scheduler] the singleton instance
|
|
39
|
+
def start
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
return instance if instance.up?
|
|
42
|
+
|
|
43
|
+
instance.start
|
|
44
|
+
end
|
|
45
|
+
instance
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Gracefully stop the scheduler and wait for running tasks.
|
|
49
|
+
def stop
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
instance.stop
|
|
52
|
+
@instance = nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] whether the scheduler loop is active
|
|
57
|
+
def running?
|
|
58
|
+
instance.up?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Array<Rufus::Scheduler::Job>] currently scheduled jobs
|
|
62
|
+
def jobs
|
|
63
|
+
instance.jobs
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Find a scheduled job by its name.
|
|
67
|
+
# @param name [String] the job name
|
|
68
|
+
# @return [Rufus::Scheduler::Job, nil]
|
|
69
|
+
def job_by_name(name)
|
|
70
|
+
jobs.find { |j| j.name == name }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def initialize
|
|
75
|
+
@rufus = nil
|
|
76
|
+
@own_mutex = Mutex.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Start the rufus-scheduler loop and register all configured tasks.
|
|
80
|
+
# @return [self]
|
|
81
|
+
def start
|
|
82
|
+
@own_mutex.synchronize do
|
|
83
|
+
return self if @rufus&.up?
|
|
84
|
+
|
|
85
|
+
require "rufus-scheduler"
|
|
86
|
+
|
|
87
|
+
@rufus = Rufus::Scheduler.new
|
|
88
|
+
|
|
89
|
+
config = Ask::Agent.configuration
|
|
90
|
+
|
|
91
|
+
# Register tasks that were configured statically
|
|
92
|
+
config.scheduler.each_task do |task|
|
|
93
|
+
schedule_task(task)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Stop the scheduler and wait for running jobs to finish.
|
|
100
|
+
def stop
|
|
101
|
+
@own_mutex.synchronize do
|
|
102
|
+
@rufus&.shutdown(:wait)
|
|
103
|
+
@rufus = nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] true if the rufus scheduler is active
|
|
108
|
+
def up?
|
|
109
|
+
@rufus&.up? || false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Array<Rufus::Scheduler::Job>] currently scheduled jobs
|
|
113
|
+
def jobs
|
|
114
|
+
@rufus&.jobs || []
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def schedule_task(task)
|
|
120
|
+
block = task[:block]
|
|
121
|
+
name = task[:name]
|
|
122
|
+
|
|
123
|
+
case task[:type]
|
|
124
|
+
when :every
|
|
125
|
+
@rufus.every task[:interval], name: name do
|
|
126
|
+
block&.call
|
|
127
|
+
end
|
|
128
|
+
when :cron
|
|
129
|
+
@rufus.cron task[:cron], name: name do
|
|
130
|
+
block&.call
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
raise ArgumentError, "Unknown schedule type: #{task[:type].inspect}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# DSL proxy used inside {Ask::Agent::Configuration#scheduler}.
|
|
139
|
+
# Collects task definitions that the {Scheduler} registers on start.
|
|
140
|
+
class SchedulerConfig
|
|
141
|
+
def initialize(config)
|
|
142
|
+
@config = config
|
|
143
|
+
@tasks = []
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Schedule a block to run on a recurring interval.
|
|
147
|
+
#
|
|
148
|
+
# @param interval [String] human-readable interval (e.g. "5 minutes", "1 hour")
|
|
149
|
+
# @param name [String, nil] optional job name for identification
|
|
150
|
+
# @yield block to execute on each tick
|
|
151
|
+
def every(interval, name: nil, &block)
|
|
152
|
+
@tasks << { type: :every, interval: interval, name: name, block: block }
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Schedule a block to run on a cron schedule.
|
|
157
|
+
#
|
|
158
|
+
# @param cron_expression [String] standard cron syntax (e.g. "0 9 * * 1-5")
|
|
159
|
+
# @param name [String, nil] optional job name for identification
|
|
160
|
+
# @yield block to execute at each scheduled time
|
|
161
|
+
def cron(cron_expression, name: nil, &block)
|
|
162
|
+
@tasks << { type: :cron, cron: cron_expression, name: name, block: block }
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Yield each configured task definition.
|
|
167
|
+
def each_task(&block)
|
|
168
|
+
@tasks.each(&block)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/ask/agent/version.rb
CHANGED
data/lib/ask/agent.rb
CHANGED
|
@@ -70,6 +70,7 @@ require_relative "agent/compactor"
|
|
|
70
70
|
require_relative "agent/hooks"
|
|
71
71
|
require_relative "agent/configuration"
|
|
72
72
|
require_relative "agent/meta_agent"
|
|
73
|
+
require_relative "agent/scheduler"
|
|
73
74
|
require_relative "agent/persistence/base"
|
|
74
75
|
require_relative "agent/persistence/in_memory"
|
|
75
76
|
require_relative "agent/skills/load_skill_tool"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0.1'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rufus-scheduler
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.9'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.9'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: minitest
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -152,6 +166,7 @@ files:
|
|
|
152
166
|
- lib/ask/agent/persistence/base.rb
|
|
153
167
|
- lib/ask/agent/persistence/in_memory.rb
|
|
154
168
|
- lib/ask/agent/reflector.rb
|
|
169
|
+
- lib/ask/agent/scheduler.rb
|
|
155
170
|
- lib/ask/agent/session.rb
|
|
156
171
|
- lib/ask/agent/skills/load_skill_tool.rb
|
|
157
172
|
- lib/ask/agent/stream_transforms/base.rb
|