ask-agent 0.6.1 → 0.8.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: f95edab12d96da68593e426829ac2507682b4b5620b5ec0b09269062bf168bd9
4
+ data.tar.gz: 7353fff33dba2ebff749df32d4c079759f1f4b2eca9193a5299fe9ec7238966c
5
5
  SHA512:
6
- metadata.gz: 9557223b85bc04778cd6821a9b7807818e06ac85124d95f79469db613a7a23d8e8317db5e6454517adf5b67fd68806eafc4ec145f8c3ea08d01de93d4f939a91
7
- data.tar.gz: 53d3a056d6c8004bdd32536f3781bae6a0bee21bd12f1af12bc3eefab02e3f229df6109c8f5f2c6802229177aa28c27a17804bb348e93c4898adce62289222d5
6
+ metadata.gz: 3e3dc6f6cae44b0c2f36e12c9253b3f21e97a17634907754cc1d2ea8930bd8bcc54b2f65ef2194dae9e1e60d4a721d8071a677f3f0bbfa0d99d68515c0da82b9
7
+ data.tar.gz: ab443967abf274b113b9ebd8f2901feffa1cf2d7e1436e6a14df8d1dbb87fa1f204d0cb97d2d567858e89631c0b08ed97bf4183174c6ffce97aee363ffa0f724
data/CHANGELOG.md CHANGED
@@ -1,3 +1,68 @@
1
+ ## [0.8.0] — 2026-07-21
2
+
3
+ ### Added
4
+
5
+ - **Provider-executed tool support in the agent loop** — `ResponseMessage` now carries a `tool_results` field for pre-computed results from provider-executed tools (e.g. OpenAI web search, file search, code interpreter). The `Loop` detects these results, adds them directly to the conversation without local execution, then proceeds with any remaining user tool calls.
6
+
7
+ ```ruby
8
+ agent = Ask::Agent.new("health_check")
9
+ agent.run("Search the web for server status")
10
+ # web_search runs on OpenAI's side; results come back pre-computed
11
+ ```
12
+
13
+ ### Changed
14
+
15
+ - **`ResponseMessage`** added `tool_results` field (default `{}`). All existing call sites are compatible via keyword argument defaults.
16
+ - **`Loop#run_turn`** — separates provider-executed results from user tool calls. Provider results are added to the conversation immediately. User tool calls continue to be executed locally via `ToolExecutor`.
17
+ - **OpenAI provider** — `split_tools` separates `Ask::ProviderTool` objects from regular tools. `format_responses_tools` converts provider tools to the Responses API format. When provider tools are present, the Responses API endpoint is used instead of Chat Completions.
18
+
19
+ ### Tested
20
+
21
+ - 13 new integration tests: loop handling with mixed tool types, provider-only tools, tool splitting, Responses API formatting.
22
+ - Full suite: 329 tests, 592 assertions — 0 failures.
23
+
24
+ ## [0.7.0] — 2026-07-21
25
+
26
+ ### Added
27
+
28
+ - **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`.
29
+
30
+ ```ruby
31
+ # agents/health_check/agent.rb
32
+ class HealthCheckAgent < Ask::Agent::Definition
33
+ model "gpt-4o"
34
+ tools :bash, :read, :grep
35
+ schedule "every 5 minutes"
36
+ end
37
+ ```
38
+
39
+ - **`Ask::Agent.new(name)`** — Create a configured `Session` from a named definition. Discovers agents from `agents/` and `app/agents/` automatically on first call.
40
+
41
+ ```ruby
42
+ agent = Ask::Agent.new("health_check")
43
+ agent.run("Check server health")
44
+ ```
45
+
46
+ - **`Ask::Agent.definitions`** — Returns all discovered definitions as a hash keyed by agent name. Each entry is `[Definition_subclass, directory_path]`.
47
+
48
+ - **`Ask::Agent.rediscover!`** — Force re-discovery when agent files change.
49
+
50
+ - **Shared tools** — `agents/shared/tools/*.rb` are auto-discovered and available to all agents in the same project.
51
+
52
+ - **`askr` CLI** — New command-line tool for running, listing, scheduling, and scaffolding agents.
53
+
54
+ ```bash
55
+ askr list # List all discovered agents
56
+ askr run health_check # Run an agent (interactive if no prompt)
57
+ askr schedule # Start the scheduler for all scheduled agents
58
+ askr new deploy_bot # Scaffold a new agent directory
59
+ ```
60
+
61
+ ### Changed
62
+
63
+ - **New dependency** — Ask::Agent::CLI module added to the lib path. `exe/askr` is registered as a gem executable.
64
+ - **Test fixture agents** added under `test/fixtures/agents/` and `test/fixtures/app/agents/` for discovery testing.
65
+
1
66
  ## [0.6.1] — 2026-07-21
2
67
 
3
68
  ### 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)
@@ -4,7 +4,14 @@ module Ask
4
4
  module Agent
5
5
  # Response message returned by {Chat#ask}.
6
6
  # Includes token counts and cost when available from the provider.
7
- ResponseMessage = Data.define(:content, :tool_calls, :thinking, :input_tokens, :output_tokens, :cost) do
7
+ # +tool_calls+ are calls that need local execution.
8
+ # +tool_results+ are pre-computed results from provider-executed tools.
9
+ ResponseMessage = Data.define(:content, :tool_calls, :tool_results, :thinking, :input_tokens, :output_tokens, :cost) do
10
+ def initialize(content:, tool_calls: {}, tool_results: {}, thinking: nil, input_tokens: nil, output_tokens: nil, cost: nil)
11
+ super(content: content, tool_calls: tool_calls, tool_results: tool_results, thinking: thinking,
12
+ input_tokens: input_tokens, output_tokens: output_tokens, cost: cost)
13
+ end
14
+
8
15
  def tool_call? = !tool_calls.empty?
9
16
  def to_s = content.to_s
10
17
  end
@@ -286,6 +293,7 @@ module Ask
286
293
  ResponseMessage.new(
287
294
  content: stream.accumulated_text,
288
295
  tool_calls: build_current_tool_calls(calls_acc),
296
+ tool_results: {},
289
297
  thinking: stream.chunks.filter_map(&:thinking).last,
290
298
  input_tokens: tokens[:input],
291
299
  output_tokens: tokens[:output],
@@ -300,9 +308,11 @@ module Ask
300
308
  input_tokens = metadata[:input_tokens] || metadata["input_tokens"]
301
309
  output_tokens = metadata[:output_tokens] || metadata["output_tokens"]
302
310
  cost = calculate_cost(input_tokens, output_tokens)
311
+ tool_results = metadata[:provider_results] || metadata["provider_results"] || {}
303
312
  ResponseMessage.new(
304
313
  content: msg.content.to_s,
305
314
  tool_calls: tool_calls,
315
+ tool_results: tool_results,
306
316
  thinking: thinking,
307
317
  input_tokens: input_tokens,
308
318
  output_tokens: output_tokens,
@@ -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
@@ -43,34 +43,56 @@ module Ask
43
43
  event_emitter.emit(Events::MessageEnd.new(tool_calls: response.tool_call?))
44
44
  @turn_count += 1
45
45
 
46
- unless response.tool_call?
46
+ # Check if there are any tool calls (user-executed or provider-executed)
47
+ has_tool_calls = response.tool_call? || (response.tool_results&.any? == true)
48
+
49
+ unless has_tool_calls
47
50
  @consecutive_tool_turns = 0
48
51
  return response.content.to_s
49
52
  end
50
53
 
51
54
  @consecutive_tool_turns += 1
52
55
 
53
- # Execute tools with concurrent result streaming
54
- tool_results = tool_executor.execute_parallel(
55
- response.tool_calls, tools, hooks, event_emitter, ToolAbortController.new
56
- ) do |tool_call_id, result|
57
- # Add each tool result as it completes (concurrent streaming)
58
- tc = response.tool_calls[tool_call_id]
59
- chat.add_message(role: :tool, content: result[:message].to_s, tool_call_id: tool_call_id) if tc
56
+ provider_results = response.tool_results || {}
57
+ all_tool_results = []
58
+
59
+ # Add provider-executed tool results directly to conversation
60
+ provider_results.each do |id, result|
61
+ chat.add_message(role: :tool, content: result[:message].to_s, tool_call_id: id)
62
+ all_tool_results << {
63
+ tool_name: result[:tool_name] || id,
64
+ message: result[:message].to_s,
65
+ status: result[:status] || "success",
66
+ provider_executed: true
67
+ }
68
+ end
69
+
70
+ # Determine which tool calls still need local execution
71
+ user_tool_calls = response.tool_calls.reject { |id, _| provider_results.key?(id) }
72
+
73
+ if user_tool_calls.any?
74
+ # Execute user tool calls locally
75
+ user_results = tool_executor.execute_parallel(
76
+ user_tool_calls, tools, hooks, event_emitter, ToolAbortController.new
77
+ ) do |tool_call_id, result|
78
+ tc = user_tool_calls[tool_call_id]
79
+ chat.add_message(role: :tool, content: result[:message].to_s, tool_call_id: tool_call_id) if tc
80
+ end
81
+ all_tool_results.concat(user_results)
60
82
  end
61
83
 
62
84
  # Check loop detection
63
- if loop_detected?(tool_results)
64
- raise LoopDetected, tool_results.last[:tool_name]
85
+ if loop_detected?(all_tool_results)
86
+ raise LoopDetected, all_tool_results.last[:tool_name]
65
87
  end
66
88
 
67
89
  if @consecutive_tool_turns >= @max_consecutive_tool_turns
68
- summary = tool_results.map { |r| r[:message].to_s.truncate(80) }.first(2).join("; ")
90
+ summary = all_tool_results.map { |r| r[:message].to_s.truncate(80) }.first(2).join("; ")
69
91
  return "Based on my investigation: #{summary}"
70
92
  end
71
93
 
72
94
  event_emitter.emit(Events::TurnEnd.new(
73
- tool_results: tool_results,
95
+ tool_results: all_tool_results,
74
96
  turn_number: @turn_count,
75
97
  input_tokens: @last_input_tokens,
76
98
  output_tokens: @last_output_tokens,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Agent
5
- VERSION = "0.6.1"
5
+ VERSION = "0.8.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.8.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