ask-agent 0.6.1 → 0.7.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: 55ad6219a4374a7aeb4688533bcf7a0a68ebb166de2d96d2e9953eb6814fe5c5
4
- data.tar.gz: 89e4e0413d96f90dd30d4203fa4023331663b2818a5df89c0a60a6975fb8bcb9
3
+ metadata.gz: 3d3eb1ad33d205f6b868c931aabf78172bc3b3d3dca0674ad67910d1ff7997a2
4
+ data.tar.gz: faff03efa8eba0d1594756745b9a29dcb8c2d51a362d818f7ec18904c3f9f92e
5
5
  SHA512:
6
- metadata.gz: 9557223b85bc04778cd6821a9b7807818e06ac85124d95f79469db613a7a23d8e8317db5e6454517adf5b67fd68806eafc4ec145f8c3ea08d01de93d4f939a91
7
- data.tar.gz: 53d3a056d6c8004bdd32536f3781bae6a0bee21bd12f1af12bc3eefab02e3f229df6109c8f5f2c6802229177aa28c27a17804bb348e93c4898adce62289222d5
6
+ metadata.gz: a9212d2d0730b6e5a9df227bb75c15a8491685b9243e6cf5f4f7511763e51a4b0bcc31c624f760e113c9e52d701863cf95664d67200bc54d67d7d9922932d6f6
7
+ data.tar.gz: 1c8664489865b480faf003c19952fc13963238e1259c9f3e7170592a31a10eca13b39cab40fa2d428274b3f3f49a405b8be817a5475b3e86fd211bcfa4c1630b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,45 @@
1
+ ## [0.7.0] — 2026-07-21
2
+
3
+ ### Added
4
+
5
+ - **Agent definitions — `Ask::Agent::Definition`** — Declarative agent configuration via subclassing. Define agents in `agents/<name>/agent.rb` or `app/agents/<name>/agent.rb`. The directory name becomes the agent name. Instructions auto-load from a sibling `instructions.md`.
6
+
7
+ ```ruby
8
+ # agents/health_check/agent.rb
9
+ class HealthCheckAgent < Ask::Agent::Definition
10
+ model "gpt-4o"
11
+ tools :bash, :read, :grep
12
+ schedule "every 5 minutes"
13
+ end
14
+ ```
15
+
16
+ - **`Ask::Agent.new(name)`** — Create a configured `Session` from a named definition. Discovers agents from `agents/` and `app/agents/` automatically on first call.
17
+
18
+ ```ruby
19
+ agent = Ask::Agent.new("health_check")
20
+ agent.run("Check server health")
21
+ ```
22
+
23
+ - **`Ask::Agent.definitions`** — Returns all discovered definitions as a hash keyed by agent name. Each entry is `[Definition_subclass, directory_path]`.
24
+
25
+ - **`Ask::Agent.rediscover!`** — Force re-discovery when agent files change.
26
+
27
+ - **Shared tools** — `agents/shared/tools/*.rb` are auto-discovered and available to all agents in the same project.
28
+
29
+ - **`askr` CLI** — New command-line tool for running, listing, scheduling, and scaffolding agents.
30
+
31
+ ```bash
32
+ askr list # List all discovered agents
33
+ askr run health_check # Run an agent (interactive if no prompt)
34
+ askr schedule # Start the scheduler for all scheduled agents
35
+ askr new deploy_bot # Scaffold a new agent directory
36
+ ```
37
+
38
+ ### Changed
39
+
40
+ - **New dependency** — Ask::Agent::CLI module added to the lib path. `exe/askr` is registered as a gem executable.
41
+ - **Test fixture agents** added under `test/fixtures/agents/` and `test/fixtures/app/agents/` for discovery testing.
42
+
1
43
  ## [0.6.1] — 2026-07-21
2
44
 
3
45
  ### Changed
data/exe/askr ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # askr — CLI for the ask-rb agent ecosystem.
5
+ #
6
+ # Usage:
7
+ # askr run <agent-name> [prompt] Run an agent with an optional prompt
8
+ # askr list List all discovered agents
9
+ # askr schedule Start the scheduler for recurring agents
10
+ # askr new <agent-name> Scaffold a new agent directory
11
+ # askr help Show this help
12
+
13
+ require "ask/agent"
14
+
15
+ Ask::Agent::CLI.run(ARGV)
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ module CLI
6
+ module_function
7
+
8
+ def run(argv)
9
+ case argv.first
10
+ when "run"
11
+ cmd_run(argv[1..])
12
+ when "list"
13
+ cmd_list
14
+ when "schedule"
15
+ cmd_schedule
16
+ when "new"
17
+ cmd_new(argv[1..])
18
+ when "help", "--help", "-h", nil
19
+ cmd_help
20
+ else
21
+ puts "Unknown command: #{argv.first}"
22
+ cmd_help
23
+ exit 1
24
+ end
25
+ end
26
+
27
+ def cmd_run(args)
28
+ name = args.first
29
+ unless name
30
+ puts "Usage: askr run <agent-name> [prompt]"
31
+ exit 1
32
+ end
33
+
34
+ prompt = args[1..]&.join(" ") || ""
35
+
36
+ begin
37
+ agent = Ask::Agent.new(name)
38
+
39
+ if prompt.empty?
40
+ puts "Starting interactive session with #{name}..."
41
+ puts "Type your message (Ctrl+D to exit):"
42
+ while (input = $stdin.gets)
43
+ response = agent.run(input.strip)
44
+ puts response
45
+ puts "---"
46
+ end
47
+ else
48
+ response = agent.run(prompt)
49
+ puts response
50
+ end
51
+ rescue Ask::UnknownAgent => e
52
+ puts e.message
53
+ exit 1
54
+ rescue => e
55
+ puts "Error: #{e.message}"
56
+ exit 1
57
+ end
58
+ end
59
+
60
+ def cmd_list
61
+ defs = Ask::Agent.definitions
62
+ if defs.empty?
63
+ puts "No agents found. Define one in agents/<name>/agent.rb or app/agents/<name>/agent.rb."
64
+ return
65
+ end
66
+
67
+ puts "Discovered agents:"
68
+ defs.each do |name, (klass, dir)|
69
+ config = klass._config
70
+ model = config[:model] || "(default)"
71
+ schedule = config[:schedule]
72
+ has_instructions = klass.instructions_path ? "yes" : "no"
73
+ tools = config[:tools].any? ? config[:tools].join(", ") : "(none)"
74
+
75
+ puts " #{name}"
76
+ puts " model: #{model}"
77
+ puts " tools: #{tools}"
78
+ puts " instructions: #{has_instructions}"
79
+ puts " schedule: #{schedule || "(none)"}" if schedule
80
+ puts " directory: #{dir}"
81
+ end
82
+ end
83
+
84
+ def cmd_schedule
85
+ defs = Ask::Agent.definitions
86
+ scheduled = defs.select { |_name, (klass, _dir)| klass._config[:schedule] }
87
+
88
+ if scheduled.empty?
89
+ puts "No scheduled agents found."
90
+ return
91
+ end
92
+
93
+ scheduled.each do |name, (klass, _dir)|
94
+ schedule = klass._config[:schedule]
95
+ puts "Scheduling #{name} (#{schedule})..."
96
+ Ask::Agent.new(name)
97
+ end
98
+
99
+ Ask::Agent::Scheduler.start
100
+ puts "Scheduler started. Running #{scheduled.length} task(s)."
101
+ puts "Press Ctrl+C to stop."
102
+
103
+ loop do
104
+ sleep 1
105
+ rescue Interrupt
106
+ puts "\nShutting down..."
107
+ Ask::Agent::Scheduler.stop
108
+ exit 0
109
+ end
110
+ end
111
+
112
+ def cmd_new(args)
113
+ name = args.first
114
+ unless name
115
+ puts "Usage: askr new <agent-name>"
116
+ exit 1
117
+ end
118
+
119
+ dir = File.join(Dir.pwd, "agents", name)
120
+ if File.exist?(dir)
121
+ puts "Directory already exists: #{dir}"
122
+ exit 1
123
+ end
124
+
125
+ FileUtils.mkdir_p(dir)
126
+
127
+ File.write(File.join(dir, "agent.rb"), <<~RUBY)
128
+ # frozen_string_literal: true
129
+
130
+ class #{camelize(name)} < Ask::Agent::Definition
131
+ model "gpt-4o"
132
+ tools :bash, :read, :grep
133
+ end
134
+ RUBY
135
+
136
+ File.write(File.join(dir, "instructions.md"), <<~MARKDOWN)
137
+ # #{name}
138
+
139
+ You are a helpful AI agent. Your goal is to assist the user with their tasks.
140
+
141
+ ## Guidelines
142
+
143
+ - Be concise and accurate
144
+ - Use the available tools when needed
145
+ - Ask for clarification if instructions are ambiguous
146
+ MARKDOWN
147
+
148
+ puts "Created agent: #{dir}"
149
+ puts ""
150
+ puts "Run it: askr run #{name}"
151
+ end
152
+
153
+ def cmd_help
154
+ puts <<~HELP
155
+ Usage: askr <command> [options]
156
+
157
+ Commands:
158
+ run <name> [prompt] Run an agent with an optional prompt
159
+ list List all discovered agents
160
+ schedule Start the scheduler for all scheduled agents
161
+ new <name> Scaffold a new agent directory
162
+ help Show this help
163
+
164
+ Examples:
165
+ askr list
166
+ askr run health_check
167
+ askr run health_check "Check the server status"
168
+ askr new deploy_bot
169
+ askr schedule
170
+
171
+ Agent directories are discovered from:
172
+ ./agents/<name>/agent.rb
173
+ ./app/agents/<name>/agent.rb
174
+ HELP
175
+ end
176
+
177
+ def camelize(str)
178
+ str.split(/[_-]/).map(&:capitalize).join
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Agent
5
+ # Declarative agent definition backed by a file convention.
6
+ #
7
+ # Subclass +Definition+ inside an +agent.rb+ file under +agents/<name>/+
8
+ # or +app/agents/<name>/+. The directory name becomes the agent name.
9
+ # Instructions load automatically from a sibling +instructions.md+.
10
+ #
11
+ # @example +agents/health_check/agent.rb+
12
+ # class HealthCheckAgent < Ask::Agent::Definition
13
+ # model "gpt-4o"
14
+ # tools :bash, :read, :grep
15
+ # schedule "every 5 minutes"
16
+ # end
17
+ #
18
+ # # agents/health_check/instructions.md is auto-loaded
19
+ #
20
+ # @example Using from Ruby
21
+ # agent = Ask::Agent.new("health_check")
22
+ # agent.run("Check server health")
23
+ class Definition
24
+ @subclasses = []
25
+
26
+ class << self
27
+ # All subclasses that have been loaded, in definition order.
28
+ attr_reader :subclasses
29
+
30
+ def inherited(subclass)
31
+ @subclasses << subclass
32
+ subclass.instance_variable_set(:@_config, { tools: [] })
33
+
34
+ # Auto-detect the directory from the file where this class is defined
35
+ # caller(1) is the file containing the `class ... < Definition` line
36
+ path = caller(1)&.first&.sub(/:.*/, "") # strip line number
37
+ if path && File.basename(path) == "agent.rb"
38
+ subclass._config[:dir] = File.dirname(path)
39
+ end
40
+
41
+ super
42
+ end
43
+
44
+ # @return [Hash] the raw config hash for this definition
45
+ def _config
46
+ @_config ||= { tools: [] }
47
+ end
48
+
49
+ # Set or get the model identifier.
50
+ def model(value = :__no_value__)
51
+ if value == :__no_value__
52
+ _config[:model]
53
+ else
54
+ _config[:model] = value
55
+ end
56
+ end
57
+
58
+ # Set tool symbols or classes.
59
+ def tools(*values)
60
+ if values.any?
61
+ _config[:tools] = values
62
+ else
63
+ _config[:tools]
64
+ end
65
+ end
66
+
67
+ # Set a cron or interval schedule for recurring runs.
68
+ def schedule(value = :__no_value__)
69
+ if value == :__no_value__
70
+ _config[:schedule]
71
+ else
72
+ _config[:schedule] = value
73
+ end
74
+ end
75
+
76
+ # Path to instructions.md relative to this definition's directory.
77
+ # Returns nil if no instructions file exists.
78
+ def instructions_path
79
+ dir = _config[:dir]
80
+ return nil unless dir
81
+
82
+ path = File.join(dir, "instructions.md")
83
+ File.exist?(path) ? path : nil
84
+ end
85
+
86
+ # Contents of the instructions.md file, or nil.
87
+ def instructions_content
88
+ path = instructions_path
89
+ path ? File.read(path) : nil
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.6.1"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
data/lib/ask/agent.rb CHANGED
@@ -18,6 +18,8 @@ module Ask
18
18
  class CompactionFailed < Error; end
19
19
  class SessionNotPersisted < Error; end
20
20
 
21
+ class UnknownAgent < Error; end
22
+
21
23
  module Extensions
22
24
  autoload :PermissionGate, "ask/agent/extensions/permission_gate"
23
25
  autoload :RateLimiter, "ask/agent/extensions/rate_limiter"
@@ -40,19 +42,167 @@ module Ask
40
42
  autoload :ExtractJson, "ask/agent/stream_transforms/extract_json"
41
43
  end
42
44
 
45
+ @registry = {}
46
+ @discovered = false
47
+ @shared_tools = {}
48
+
43
49
  class << self
44
- def configuration
45
- @configuration ||= Configuration.new
50
+ # All discovered agent definitions, keyed by name.
51
+ # Triggers discovery on first call.
52
+ # @return [Hash<String, Array(Class, String)>] name → [Definition subclass, directory path]
53
+ def definitions
54
+ discover!
55
+ @registry.dup
56
+ end
57
+
58
+ # Create a new agent session from a named definition.
59
+ #
60
+ # @param name [String, Symbol] the agent name (directory name under +agents/+)
61
+ # @return [Session] a configured, ready-to-run session
62
+ def new(name)
63
+ discover!
64
+ entry = @registry[name.to_s]
65
+ raise UnknownAgent, "Unknown agent: #{name.inspect}. Searched agents/ and app/agents/." unless entry
66
+
67
+ klass, dir = entry
68
+ build_session_from_definition(klass, dir)
46
69
  end
47
70
 
48
- def configure
49
- yield configuration
71
+ # Force re-discovery of agent definitions.
72
+ def rediscover!
73
+ @discovered = false
74
+ @registry = {}
75
+ discover!
50
76
  end
51
77
 
52
- def load_extensions
53
- Dir[File.expand_path("agent/extensions/*.rb", __dir__)].each { |f| require f }
54
- rescue Errno::ENOENT
78
+ # Paths where agent directories are discovered.
79
+ def default_agent_paths
80
+ [
81
+ File.join(Dir.pwd, "agents"),
82
+ File.join(Dir.pwd, "app", "agents")
83
+ ]
55
84
  end
85
+
86
+ # Resolve a tool symbol to a tool class.
87
+ # Checks the shared tools directory first, then falls back to the
88
+ # global tool registry from ask-tools.
89
+ def resolve_tool_symbol(symbol)
90
+ @shared_tools[symbol.to_s] || begin
91
+ Ask::Tools[symbol.to_s]
92
+ rescue StandardError
93
+ nil
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def discover!
100
+ return if @discovered
101
+ @discovered = true
102
+
103
+ paths = default_agent_paths
104
+ paths.each do |base|
105
+ next unless File.directory?(base)
106
+
107
+ # Discover shared tools
108
+ discover_shared_tools(base)
109
+
110
+ # Discover agent directories
111
+ Dir["#{base}/*/agent.rb"].sort.each do |file|
112
+ dir = File.dirname(file)
113
+ name = File.basename(dir)
114
+ next if name == "shared"
115
+
116
+ require file
117
+
118
+ # Find the Definition subclass whose directory matches
119
+ match = Definition.subclasses.find { |klass|
120
+ klass._config[:dir] == dir
121
+ }
122
+
123
+ if match
124
+ @registry[name] = [match, dir]
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def discover_shared_tools(base)
131
+ shared_tools_dir = File.join(base, "shared", "tools")
132
+ return unless File.directory?(shared_tools_dir)
133
+
134
+ Dir["#{shared_tools_dir}/*.rb"].sort.each do |file|
135
+ tool_name = File.basename(file, ".rb")
136
+ require file
137
+
138
+ # Find the Ask::Tool subclass that was just loaded
139
+ # (it registered itself in Ask::Tools on require)
140
+ @shared_tools[tool_name] = tool_name
141
+ end
142
+ end
143
+
144
+ def build_session_from_definition(klass, dir)
145
+ config = klass._config
146
+ session_opts = { model: config[:model] || Ask::Agent.configuration.default_model }
147
+
148
+ # Resolve tools
149
+ tools = resolve_definition_tools(config[:tools], dir)
150
+ session_opts[:tools] = tools if tools.any?
151
+
152
+ # Load instructions
153
+ prompt = klass.instructions_content
154
+ session_opts[:system_prompt] = prompt if prompt
155
+
156
+ # Apply schedule if defined
157
+ schedule = config[:schedule]
158
+ if schedule
159
+ task_block = ->(sess = nil) {
160
+ agent = build_session_from_definition(klass, dir)
161
+ agent.run("")
162
+ }
163
+ Ask::Agent.configuration.scheduler.every(schedule, name: File.basename(dir), &task_block)
164
+ end
165
+
166
+ Session.new(**session_opts)
167
+ end
168
+
169
+ def resolve_definition_tools(tool_specs, dir)
170
+ tools = []
171
+ tool_specs.each do |spec|
172
+ case spec
173
+ when Symbol, String
174
+ name = spec.to_s
175
+ # Try per-agent tools directory
176
+ agent_tool_path = File.join(dir, "tools", "#{name}.rb")
177
+ if File.exist?(agent_tool_path)
178
+ require agent_tool_path
179
+ end
180
+
181
+ resolved = resolve_tool_symbol(name)
182
+ if resolved
183
+ tool_class = resolved.is_a?(Class) ? resolved : Ask::Tools[name]
184
+ tools << tool_class if tool_class
185
+ end
186
+ when Class
187
+ tools << spec
188
+ end
189
+ end
190
+ tools
191
+ end
192
+ end
193
+
194
+ # Register the global config
195
+ def self.configuration
196
+ @configuration ||= Configuration.new
197
+ end
198
+
199
+ def self.configure
200
+ yield configuration
201
+ end
202
+
203
+ def self.load_extensions
204
+ Dir[File.expand_path("agent/extensions/*.rb", __dir__)].each { |f| require f }
205
+ rescue Errno::ENOENT
56
206
  end
57
207
  end
58
208
  end
@@ -70,10 +220,12 @@ require_relative "agent/compactor"
70
220
  require_relative "agent/hooks"
71
221
  require_relative "agent/configuration"
72
222
  require_relative "agent/meta_agent"
73
- require_relative "agent/scheduler"
74
223
  require_relative "agent/persistence/base"
75
224
  require_relative "agent/persistence/in_memory"
76
225
  require_relative "agent/skills/load_skill_tool"
226
+ require_relative "agent/scheduler"
227
+ require_relative "agent/definition"
228
+ require_relative "agent/cli"
77
229
 
78
230
  # Test helpers (loaded on demand)
79
231
  autoload :Test, "ask/agent/test"
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -139,18 +139,22 @@ description: Agent loop, session management, tool execution, context compaction,
139
139
  and extensions.
140
140
  email:
141
141
  - kaka@myrrlabs.com
142
- executables: []
142
+ executables:
143
+ - askr
143
144
  extensions: []
144
145
  extra_rdoc_files: []
145
146
  files:
146
147
  - CHANGELOG.md
147
148
  - LICENSE
148
149
  - README.md
150
+ - exe/askr
149
151
  - lib/ask-agent.rb
150
152
  - lib/ask/agent.rb
151
153
  - lib/ask/agent/chat.rb
154
+ - lib/ask/agent/cli.rb
152
155
  - lib/ask/agent/compactor.rb
153
156
  - lib/ask/agent/configuration.rb
157
+ - lib/ask/agent/definition.rb
154
158
  - lib/ask/agent/events.rb
155
159
  - lib/ask/agent/extensions/audit_log.rb
156
160
  - lib/ask/agent/extensions/permission_gate.rb