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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a1f33fb0145ac0a1abf50990d15a6f4aa288a67fe8ec11f478cead3b9401a43
4
- data.tar.gz: ebaa94902c33f6fe351b97108c6db085096a92ca4206706c8197cba5aaa5d8d5
3
+ metadata.gz: 55ad6219a4374a7aeb4688533bcf7a0a68ebb166de2d96d2e9953eb6814fe5c5
4
+ data.tar.gz: 89e4e0413d96f90dd30d4203fa4023331663b2818a5df89c0a60a6975fb8bcb9
5
5
  SHA512:
6
- metadata.gz: 3a32c8330c3d443980a48df17507cdb4cfa56e67129f7e0fbabce12d9911705a621b85b60f7849ccd422496f46f2c3cc3e4f639cef11673f6530fbc5bd46480e
7
- data.tar.gz: 2fd18590c038a8c1221c6422980aebff1edd38e634d53036cb231cae0eda36d15a9df8bcbb48cda146dd828670ebdbf10d9c88bc388b89e0f6fe65c8d7c3788e
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- raise NotImplementedError
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
- @store = {}
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.1"
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.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