ask-agent 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a1f33fb0145ac0a1abf50990d15a6f4aa288a67fe8ec11f478cead3b9401a43
4
- data.tar.gz: ebaa94902c33f6fe351b97108c6db085096a92ca4206706c8197cba5aaa5d8d5
3
+ metadata.gz: 0f2eeb2cf5c59ddd76d509d628a809ebe903d9881f5dca8617f511331baaecf7
4
+ data.tar.gz: 25d07e7d421013bef9ba175011461cbdab6d8370d81e54e8fc742429bbe5473d
5
5
  SHA512:
6
- metadata.gz: 3a32c8330c3d443980a48df17507cdb4cfa56e67129f7e0fbabce12d9911705a621b85b60f7849ccd422496f46f2c3cc3e4f639cef11673f6530fbc5bd46480e
7
- data.tar.gz: 2fd18590c038a8c1221c6422980aebff1edd38e634d53036cb231cae0eda36d15a9df8bcbb48cda146dd828670ebdbf10d9c88bc388b89e0f6fe65c8d7c3788e
6
+ metadata.gz: 36eab4ddb395657b6bac444f4347cdf783d130e4370c300da944c9636e87ac6ee310b3af98029e2aaffa318d7632a6ae5a89c7168e4a7ae8b247f4b640ba40da
7
+ data.tar.gz: f2926d63179b15cc08706c4e5b29a4b5f5a8cf32015ad4d62e266eaff6049951d7afe238eb7b2340038dcee0c14f602ad37b9651b801b2c9a775997055df3851
data/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## [0.6.0] — 2026-07-21
2
+
3
+ ### Added
4
+
5
+ - **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.
6
+
7
+ ```ruby
8
+ Ask::Agent.configure do |c|
9
+ c.scheduler.every "5 minutes", name: "health-check" do
10
+ Ask::Agent::Session.new(model: "gpt-4o").run("Check server health")
11
+ end
12
+
13
+ c.scheduler.cron "0 9 * * 1-5", name: "morning-report" do
14
+ Ask::Agent::Session.new(model: "gpt-4o").run("Generate daily report")
15
+ end
16
+ end
17
+
18
+ Ask::Agent::Scheduler.start # background thread loop
19
+ Ask::Agent::Scheduler.stop # graceful shutdown
20
+ ```
21
+
22
+ Manage the scheduler at runtime:
23
+ - `Ask::Agent::Scheduler.running?` — check if the loop is active
24
+ - `Ask::Agent::Scheduler.jobs` — list all scheduled jobs (returns `Rufus::Scheduler::Job` objects with `.name`, `.next_time`, etc.)
25
+ - `Ask::Agent::Scheduler.job_by_name("health-check")` — find a specific job
26
+ - Tasks without blocks are valid — they register but execute nothing
27
+
28
+ Powered by `rufus-scheduler` (added as a runtime dependency). The scheduler is optional — users who don't configure any tasks are unaffected.
29
+
30
+ ### Changed
31
+
32
+ - **`Ask::Agent::Configuration`** now exposes `#scheduler` returning a `SchedulerConfig` DSL proxy. No breaking changes for existing users.
33
+ - **Gemspec** — added `rufus-scheduler ~> 3.9` as a runtime dependency.
34
+
1
35
  ## [0.5.0] — 2026-07-21
2
36
 
3
37
  ### 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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
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.5.0
4
+ version: 0.6.0
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